Blog

Swift Tip: Auto Layout with Key Paths

When key paths were introduced with Swift 4, we took a moment to show how they could be used to create very simple, but very powerful helper functions to create Auto Layout constraints. You can see the results in Swift Talk 75 (a public episode).

Preparing for new episodes, we’ve had the chance to use this approach again, and we still really like it. 😀

The approach uses key paths to describe layout anchors on views. To create a valid constraint from two anchors, they have to refer to the same axis; for example, we can constrain a left anchor to another left anchor, but we cannot constrain a left anchor to a top anchor. Using generics, we can ensure the compatibility of the anchors in our helper functions.

We’ll start with an equal helper that constrains the same anchor type on two views to each other:

func equal<L, Axis>(_ to: KeyPath<UIView, L>) -> (UIView, UIView) -> NSLayoutConstraint where L: NSLayoutAnchor<Axis> {
    return { view1, view2 in
        view1[keyPath: to].constraint(equalTo: view2[keyPath: to])
    }
}

equal is a function that returns a function, which takes the two views and produces a layout constraint. To clean up the function signature, we introduce a type alias for this return type:

typealias Constraint = (UIView, UIView) -> NSLayoutConstraint

func equal<L, Axis>(_ to: KeyPath<UIView, L>) -> Constraint where L: NSLayoutAnchor<Axis> {
    return { view1, view2 in
        view1[keyPath: to].constraint(equalTo: view2[keyPath: to])
    }
}

The parameter of equal is a key path describing a layout anchor property on UIView. You can read the KeyPath<UIView, L> type as a key path that can be used on a UIView to get a property of type L, which in this case is constrained by the where clause to be an NSLayoutAnchor.

We can use this function to create constraints like this:

let constraint = equal(\.topAnchor)(view1, view2)

We usually use these helpers together with a custom addSubview method. This method combines adding a subview with disabling its autoresizing mask translation, and applying the constraints:

extension UIView {
    func addSubview(_ other: UIView, constraints: [Constraint]) {
        other.translatesAutoresizingMaskIntoConstraints = false
        addSubview(other)
        addConstraints(constraints.map { $0(other, self) })
    }
}

Using our variant of addSubview, we can add a view that’s constrained on all four edges to its parent like this:

addSubview(childView, constraints: [
    equal(\.topAnchor), equal(\.bottomAnchor),
    equal(\.leftAnchor), equal(\.rightAnchor)
])

Of course, we don’t always want to constrain the same anchor on two views. We can write another variant of equal that accepts two key paths, making sure both refer to a layout anchor on the same axis:

func equal<L, Axis>(_ from: KeyPath<UIView, L>, _ to: KeyPath<UIView, L>, constant: CGFloat = 0) -> Constraint where L: NSLayoutAnchor<Axis> {
    return { view1, view2 in
        view1[keyPath: from].constraint(equalTo: view2[keyPath: to], constant: constant)
    }
}

Now we can write our original equal function with only one key path parameter in terms of the second, more complete one:

func equal<L, Axis>(_ to: KeyPath<UIView, L>, constant: CGFloat = 0) -> Constraint where L: NSLayoutAnchor<Axis> {
    return equal(to, to, constant: constant)
}

Similarly, we can create another variant that only accepts key paths to dimension anchors (e.g. heightAnchor):

func equal<L>(_ keyPath: KeyPath<UIView, L>, constant: CGFloat) -> Constraint where L: NSLayoutDimension {
    return { view1, _ in
        view1[keyPath: keyPath].constraint(equalToConstant: constant)
    }
}

Here’s an example combining the use of multiple variants of equal. We’re adding a subview that’s halfway off screen to the bottom, is inset ten points at both sides, and is 100 points high:

addSubview(childView, constraints:[
    equal(\.centerYAnchor, \.bottomAnchor),
    equal(\.leftAnchor, constant: 10), equal(\.rightAnchor, constant: -10),
    equal(\.heightAnchor, constant: 100)
])

The code for these helpers is only 30 lines long, and could easily be extended with very little additional code to support all of auto layout.

For more episodes on Swift, the language, see our Swift Talk Collection; over half the episodes are public!

To access all Swift Talk episodes, become a Subscriber — we have monthly or yearly plans, for individuals or teams, with new episodes every week. 🤓

Stay up-to-date with our newsletter or follow us on Twitter.

Back to the Blog

recent posts