Programmatic layout is great, but Apple’s Auto Layout is a bit heavy. Apples take on this:

Whenever possible, use Interface Builder to set your constraints.

Not the best advice considering the previously discussed benefits of programmatic layouts.

Many frameworks exist to alleviate the problem. SnapKit, PureLayout and Anchorage are all viable options.

In most cases introducing relevant dependencies is fine and has many benefits but the negatives are:

  1. requiring dependency management (SPM, CocoaPods, Carthage)
  2. adding additional imports
  3. unneeded code bloat, such as importing Alamofire for one simple network request

I build many little projects. Most of my layouts are simple. So I don’t want the burden of an external dependency.

I wondered if I could build a simple, one file, auto layout DSL (Domain Specific Language) that I could copy and paste into my projects.

If you want to skip to the result check out the file.

Goal

The goal is to replace lengthy repetitive code with short concise code. Take the below example where I add a subview and pin all its edges to its superview.

view.addSubview(subview)
subview.translatesAutoresizingMaskIntoConstraints = false
subview.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
subview.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
subview.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
subview.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true

I’d prefer to do this.

subview.place(on: view).pin(.allEdges)

It declutters Apples API. Where they use words like “constraint” and “anchor”, I use “pin” and “edge”. The words are largely interchangeable but I like the fact “pin” is six fewer characters than “constrain”. (Ironically Apple’s documentation often talks about “pinning edges” rather than “constraining to anchors”).

I also plan to account for some common scenarios such as:

  • Padding
  • Pinning to views that aren’t the superview
  • Pinning to layout guides
  • Pinning top edges to bottom edges or leading edges to trailing edges
  • Fixing heights and widths

I’ll omit more complex scenarios for now.

NSLayoutConstraint

NSLayoutConstraint is our starting point. Its initialiser takes many parameters. I wrapped the initialiser in a convenience initialiser to reduce code and focus on the parameters I care about.

Here’s how it looks with comments laying out (pardon the pun) what’s happening in a constraint.

extension NSLayoutConstraint {
    convenience init(item: Any, attribute: Attribute, toItem: Any? = nil, toAttribute: Attribute = .notAnAttribute, constant: CGFloat) {
        self.init(item: item, // first views
                  attribute: attribute, // leading, trailing etc.
                  relatedBy: .equal, // is equal to
                  toItem: toItem, // superview or other views
                  attribute: toAttribute, // leading, trailing
                  multiplier: 1, // default is 1 
                  constant: constant) // with padding
    }
}

Building Blocks

The building block of my DSL is an enum with associated values. The associated values align with the convenience initialiser I created. Meaning I can build constraints from my enum.

enum Constraint {
    case relative(NSLayoutConstraint.Attribute, CGFloat, to: Anchorable? = nil, NSLayoutConstraint.Attribute? = nil)
    case fixed(NSLayoutConstraint.Attribute, CGFloat)
    case multiple([Constraint])
}

I also introduced the protocol Anchorable, i.e. something that can be anchored to. Originally this was just UIView but to increase the scope of my DSL I extend UIView and UILayoutGuide to conform to Anchorable. This allows for code such as

view.pin(to: superview.safeAreaLayoutGuide)

Back to the constraint enum. Looking at it case by case.

  • relative has associated values for two NSLayoutConstraint.Attributes. This allows for pinning an edge of one type to that of another type (e.g. leading to trailing or top to bottom). The second attribute is optional. If it is nil I pin the same attribute type i.e. leading to leading. relative also has associated values Anchorable? , in case I want to pin to a view other than superview, and a CGFloat for padding.
  • fixed has associated values for an attribute and CGFloat. It is used to pin fixed widths and heights, which don’t need the additional attributes provided by relative.
  • multiple has the associated value of an array of Constraints. It will provide the power to create convenient helpers like .pin(to: .allEdges) that use multiple constraints.

The Constraint means we can create more concise versions of NSLayoutConstraint.

.relative(.top, 10, to: view, .bottom)

Creating the above instance from inside a UIView would provide all the data we need to build a full NSLayoutConstraint and apply it.

Place

The place method is in extension on UIView. It looks like this

@discardableResult
func place(on view: UIView) -> UIView {
    view.addSubview(self)
    self.translatesAutoresizingMaskIntoConstraints = false
    return self
}

It is a simple alias for adding a subview and turning off translatesAutoresizingMaskIntoConstraints, which we want for programmatic layouts. We return the view so that we can chain the pinning method e.g.

view.place(on: superview).pin(.fixedHeight(50))

Using @discardableResult means the place method can be used alone while preventing the Xcode warning

Result of call to 'place(on:)' is unused

Pin

The pin method is also in an extension on UIView. It looks like this.

@discardableResult
func pin(_ constraints: Constraint...) -> UIView {
    self.translatesAutoresizingMaskIntoConstraints = false
    apply(constraints)
    return self
}

It is another opportunity to turn off translatesAutoresizingMaskIntoConstraints just in case addSubview was used instead of place(on:) before pinning.

The pin method takes one variadic parameter of type Constraint. The variadic parameter takes 0 or more values and creates an array of those values in your function body. The benefit of using a variadic parameter instead of an array is that consumers of the API need not include angular braces.

// array
view.pin([.top, .leading, .trailing, .bottom])

// variadic parameter
view.pin(.top, .leading, .trailing, .bottom)

The pin method also returns the view after constraints are applied just in case someone is aggressively verbose e.g. view.pin(.top).pin(.bottom).

The main point of the pin method is to call apply(constraints). apply is private to the UIView extension. It loops through an array of provided constraints, creating actual NSLayoutConstraints and making them active.

private func apply(_ constraints: [Constraint]) {
    for constraint in constraints {
        switch constraint {
        case .relative(let attribute, let constant, let toItem, let toAttribute):
            NSLayoutConstraint(item: self,
                               attribute: attribute,
                               toItem: toItem ?? self.superview!,
                               toAttribute: toAttribute ?? attribute,
                               constant: constant).isActive = true
        case .fixed(let attribute, let constant):
            NSLayoutConstraint(item: self,
                               attribute: attribute,
                               constant: constant).isActive = true
        case .multiple(let constraints):
            apply(constraints)
        }
    }
}

Each Constraint case provides the necessary data to build the NSLayoutConstraint. When certain optionals are omitted we fall back to defaults. If no toItem is provided, it assumes the consumer means the superview. When no toAttribute is provided, it assumes the consumer means to pin attribute to attribute, e.g. leading to leading.

This is what gives the consumer convenient ways to create simple constraints.

view.pin(.relative(.top, 10))
view.pin(.relative(.top, 10, to: anotherView))
view.pin(.relative(.top, 10, to: anotherView, .bottom))

Factory Functions

The final pieces of the puzzle are the convenient factory functions that create specific constraints. All the factory functions create various instances of the Constraint enum. Here is an example.

static func top(to anchors: Anchorable? = nil, padding:  CGFloat = 0) -> Constraint {
    .relative(.top, padding, to: anchors)
}
static func bottom(to anchors: Anchorable? = nil, padding:  CGFloat = 0) -> Constraint {
    .relative(.bottom, -padding, to: anchors)
}

The factory functions make the API more concise. One problem is if consumers omit all parameters the resultant API looks like .pin(to: .top()). The parentheses in top() are superfluous. That’s why static constants are included that call our functions with no parameters like so

static let top: Constraint = .top()
static let bottom: Constraint = .bottom()

Now consumers can omit the ().

view.pin(.top)

This pins the top of the view to its superview.

Explicit Padding

Note that our static functions force consumers to use the parameter name padding. I originally omited this but found that Xcode’s predictive typing was pretty poor with multiple unnamed parameters. By adding the padding parameter name, Xcode’s autocomplete provides padding and looks for CGFloats. This is useful when something like let padding = CGFloat(20) exists as Xcode will predict it.

Conclusion

I am pretty happy with it. It’s 147 lines of predominantly factory-style code that will save me a lot of time creating simple layouts and mean I won’t need to download dependencies.

The file is stored in this repo. Here’s a snapshot of some things it can do.

// place subview on view an pin all edges i.e. top, bottom, leading, trailing
subview.place(on: view).pin(.allEdges)
// as above with padding
subview.place(on: view).pin(.allEdges(padding: 20))
// as above with no padding and using specific parameters
subview.place(on: view).pin(.top, .leading, .trailing, .bottom)


// if you don't want to chain constraints after placement, simply place first
subview.place(on: view)
// pin top edge to a different views bottom with padding
subview.pin(.top(to: anotherView, .bottom, padding: 10))
// pin horizontal edges to superview
subview.pin(.horizontalEdges)
// pinning top and bottom edges to layout guide and to superview leading and trailing
subview.pin(.top(to: view.safeAreaLayoutGuide), 
            .bottom(to: view.safeAreaLayoutGuide),
            .leading, 
            .trailing)
// pinning fixed height and width in center of superview
subview.pin(.fixedHeight(50), .fixedWidth(50), .centerX, .centerY)