Part 3: Writing an Auto Layout DSL with Swift’s Operator Overloading and Result Builders 🏗️

Simeon Rumyannikov
7 min readNov 25, 2022

--

You can also watch this article here:

Last time we built the main part of the DSL, allowing the end user to express constraints in an arithmetic fashion.

Part 1 and 2 Result.

In this article we will expand on this to include:

  • Combined anchors — size, center, horizontalEdges, verticalEdges
  • Insets — allow users to set insets on combined edge anchors
  • Result Builder to handle the output of different anchors, allowing easy constraint batching and activation.

Step 10: New anchor type. Create a protocol wrapping 2 LayoutAnchor types.

First we create a type to hold a pair of anchors that conform to the LayoutAnchor protocol defined in Part 1.

Here I considered modifying the LayoutBlock to hold an array of anchors. Then, when generating a constraint from 2 such blocks, we could zip the anchors together and iterate over them, constraining the anchors to each other and passing in the relevant constants/multipliers.

There are 2 disadvantages:

  • Even single expressions with basic anchors would return an array of constraints. The complicates the DSL from the users point of view.
  • A user might try and use a composite anchor (with 2 or 4 anchors) with a singular anchor. We can handle this by ignoring the additional anchors. The compiler will produce no warning. However, these operations and resultant constraints will be meaningless. This has the potential to introduce frustrating bugs into the end users code — something we want to avoid!!

Using a concrete LayoutAnchorPair solves the first issue by allowing us to overload operators separately for different anchor types. Specifically, we will overload operators seperatley for the LayoutAnchorPair types. Similarly the second issue is solved because the compiler will now throw an error because an overload operator is used for undefined input types.

Step 11: Extend the LayoutAnchorPair protocol.

This extension will serve the same purpose as the LayoutAnchor protocol extension defined earlier — it acts as a wrapper which calls the methods of the base type and returns the resultant constraints.

The key difference here is that each method returns an array of constraints, which combines the constraints of the 2 LayoutAnchor types.

Because the anchors we pass to the LayoutAnchorPair are constrained to LayoutAnchor types, we can provide a default implementation easily.

Additionally, instead of constants, these methods take a EdgeInsetPair which will offer the ability to supply different constants to each of the constraints.

Each method maps constant1 to constrain anchor1 and constant2 on anchor2.

Step 13: Create concrete LayoutAnchor types.

In Part 1 & 2 we did not have to create concrete LayoutAnchor types since we just made the default NSLayoutAnchors conform to the LayoutAnchor protocol. Here though, we need to provide our own anchors that conform to the AnchorPair protocol.

A reminder of typealiases used previously:

We define a nested type which satisfies the EdgeInsetPair protocol. This satifies the AnchorPair associated type requirement — Insets. The concrete type adhering to this protocol will be used during operation overloading to set insets.

We use computed properties to conform to the LayoutAnchorPair and EdgeInsetPair protocol. These computed properties return the internal properties of the LayoutAnchorPair and EdgeInsetPair.

Here it is important to insure that the constants supplied by the inset type match the anchors defined on this anchor pair. Specifically in the context of the extension defined in the last step, where constant1 is used to constrain anchor1.

This “generic” protocol allows us to define one protocol extension which is valid for all anchor pairs. Provided we follow the rule discussed above. At the same time we can use more meaningful anchor specific labels — bottom/top — outside of the extension. Such as when defining operator overloads.

An alternate solution would entail having separate extensions for all types — which isn’t too bad since the number of anchors is limited. But I was to lazy here and tried to create an abstract solution. Either way, this is an implementation detail which is internal to the library and can be changed in the future without breaking changes.

Please leave a comment if you belive a diffrent design would be optimal.

More architectural decision points:

  • Insets must be a separate types as they are initialised and used outside of an anchor.
  • Nested types are used to protect namespace and keep it clean, whilst also highlighting the fact that an Inset type implementation depends on the specific PairAnchor implementation.

For brevity I only show here the implementation of the YAxisAnchorPair. At the end I link the source code where you can view the implementation of XAxisAnchorPair, CenterAnchorPair and SizeAnchorPair.

Note: Adding insets is not the same as adding constants to an expression.

You might have noticed that we added a minus operator to the top constant before returning it as part of the EdgePair interface. Similarly, XAxisAnchorPair implementations adds a minus to the trailing anchor. Inverting the constants means that insets will function as insets rather than shifting each edge in the same same direction.

Red view is constrained to 200 in image one and two.

In in the left image, all edge anchors of blue view are set to be equal to that of red view plus a 40. Which results in blue view being the same size but shifted by 40 along both axis. Whilst this makes sense in terms of constraints, this is not a common operation in itself.

Setting insets around a view or adding padding around a view is much more common. Hence in the context of providing an API for combined actors makes more sense.

In the right image, we set blue view to be equal to red view plus an inset of 40 using this DSL. This is achieved my the inversion of constants described above.

Step 14: Extend the View & LayoutGuide to initialise composite anchors.

Just like we did before, we extend View and LayoutGuide types with computed properties which initialise LayoutBlocks when called.

Layout Blocks are initialised with the new LayoutAnchorPair anchor type.

Step 15: Overload the +/- operators to allow expressions with edge insets.

For horizontal edge and vertical edge anchors we want the user to be able to specify insets. To achieve this we extend the UIEdgeInsets type because it is already familiar to most users of this DSL.

The extension allows initialisation with just top/bottom or left/right insets — the rest defaulting to 0.

We also need to add a new property to LayoutBlock to store edgeInsets.

Add a property to store edge pair insets. This is optional and will have to be resolved to a value made from constants.

Next we overload operators for inputs: LayoutBlock with UIEdgeInsets.

We map the instance of UIEdgeInsets provided by the user, to the relevant nested type defined as part of concrete LayoutAnchorPair.

Extra or incorrect paramaters passed in by the user as part of the UIEdgeInset type will be ignored.

Step 15: Overload Comparison Operators to define constraint relations.

The principle remains the same as before. We overload relations operators for LayoutBlocks and LayoutAnchorPair inputs.

If a user provides a pair of edge insets — we use them, otherwise we generate a pair of generic insets from LayoutBlocks constants. The generic inset struct is a wrapper, unlike other insets it does not negate one of the sides.

Step 16: Dimension LayoutAnchorPair

Just like a single dimension anchor, a pair of dimension anchors — widthAnchor and heightAnchor — can be constrained to constants. Therefore, we have to provide sepererate operator overloads to handle this use case.

  • Allow the user of the DSL to fix both height and width to the same constant — creating a square.
  • Allow the user of the DSL to fix the SizeAnchorPair to a CGSize type — makes more sense in most cases as views are not squares.

The compiler is aware that the underlying types are Dimension types, hence the constraint() methods are available to use.

Step 17: Using Result Builders to handle [NSLayoutConstraint] and NSLayoutConstraint types.

Composite anchors create an interesting problem. Using these anchors in an expression results in an array of constraints. This might get messy for the end user of the DSL.

We want to provide a way to batch together these constraints without boilerplate code and activate them effectively — in one go rather than individually or in separate batches.

Introduced in Swift 5.4 result builders (also known as function builders) allow you to build up a result using ‘build blocks’ implicitly from a series of components. In fact, they are the underlying building block behind Swift UI.

In this DSL the final result is an array of NSLayoutConstraint objects.

We provide build functions, which allow the result builder to resolve individual constraints and arrays of constraints into one array. All of this logic is hidden from the end user of the DSL.

Most of these functions I copied directly from the swift-evolution result builder proposal example. I added unit tests to ensure they work correctly in this DSL.

Putting all this results in the following:

Result builders also allow us to include additional control flow within the closure, which would not be possible with an array.

Thats it, thank you for reading! This article took many days to write — so if you learned something new I would appreciate a ⭐ on this repo!

If you have any advice for me, don’t be shy: 💬 and share your experience!

The Final version of this DSL includes a four dimensional anchor which is made out of a pair of AnchorPair types…

You can find all the code here:

--

--