This episode is freely available thanks to the support of our subscribers

Subscribers get exclusive access to new and all previous subscriber-only episodes, video downloads, and 30% discount for team members. Become a Subscriber

We clean up our layout code by introducing helper functions that leverage Swift's key paths.

00:06 Today we'll revisit the code from the last episode, and without adding any features, we're going to improve the layout code. We generally use the same pattern for every view: we add the subview, set translatesAutoresizingMaskIntoConstraints to false, and add a bunch of constraints. Let's see what happens when we combine these actions in a single method.

addSubview with Constraints

00:57 We already use our custom addSubview method of Box, so it's easy to extend this method by taking a constraints array:

extension Box where A: UIView {
    func addSubview<V: UIView>(_ view: Box<V>, constraints: [NSLayoutConstraint]) {
        unbox.addSubview(view.unbox)
        references.append(view)
        view.unbox.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate(constraints)
    }
}

01:38 By passing in the constraints to the addSubview method, we're already able to eliminate some lines in viewDidLoad:

rootView.addSubview(trackInfoBox, constraints: [
    trackInfoBox.leftAnchor.constraint(equalTo: view.leftAnchor),
    trackInfoBox.rightAnchor.constraint(equalTo: view.rightAnchor),
    trackInfoBox.heightAnchor.constraint(equalToConstant: trackInfoViewHeight)
])

// ...

rootView.addSubview(box, constraints: [
    loadingIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
    loadingIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])

// ...

mapView.addSubview(boxedButton, constraints: [
    button.topAnchor.constraint(equalTo: _mapView.safeAreaLayoutGuide.topAnchor),
    button.trailingAnchor.constraint(equalTo: _mapView.safeAreaLayoutGuide.trailingAnchor)
])

02:26 There's still a lot of repetition: we almost always use the new subview on the left-hand side of the constraint and the superview. We often also use the same anchors for both views — the child's left anchor is constrained to the parent's left anchor, the middle anchor to the middle, etc.

Simplifying Common Constraints

03:03 We create the type alias Constraint for a function that takes a child view and a parent view and returns a layout constraint between the two:

typealias Constraint = (_ child: UIView, _ parent: UIView) -> NSLayoutConstraint

03:36 Now we can write a few small functions that construct the most frequently used constraints:

func equalCenterX(child: UIView, parent: UIView) -> NSLayoutConstraint {
    return child.centerXAnchor.constraint(equalTo: parent.centerXAnchor)
}

func equalCenterY(child: UIView, parent: UIView) -> NSLayoutConstraint {
    return child.centerYAnchor.constraint(equalTo: parent.centerYAnchor)
}

04:34 Another new variant of addSubview can now take an array of Constraint functions and call each of them with the two views:

extension Box where A: UIView {
    func addSubview<V: UIView>(_ view: Box<V>, constraints: [Constraint]) {
        unbox.addSubview(view.unbox)
        references.append(view)
        view.unbox.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate(constraints.map { c in
            c(view.unbox, unbox)
        })
    }
}

05:09 We can only use this method to construct constraints between the view and the superview we're adding it to, but most of the layout code deals with exactly this situation.

05:21 The loading indicator layout code gets really short if we use Constraint functions:

rootView.addSubview(box, constraints: [
    equalCenterX,
    equalCenterY
])

Constraint Helpers with KeyPaths

06:11 We could keep writing functions that combine various anchors, but Swift's key paths can help us write a more flexible function that creates constraint combinators for us. By giving the function two key paths from a UIView to an NSLayoutAnchor, it can return a Constraint combinator:

func equal(_ from: KeyPath<UIView, NSLayoutAnchor>, _ to: KeyPath<UIView, NSLayoutAnchor>) -> Constraint {

}

08:11 But this notation won't work, because NSLayoutAnchor is a generic class that needs an axis parameter:

func equal<Axis>(_ from: KeyPath<UIView, NSLayoutAnchor<Axis>>, _ to: KeyPath<UIView, NSLayoutAnchor<Axis>>) -> Constraint {

}

08:36 In order to make the method generic over any subclass of NSLayoutAnchor, we have to pull the type parameter into a where clause:

func equal<Axis, L>(_ from: KeyPath<UIView, L>, _ to: KeyPath<UIView, L>) -> Constraint  where L: NSLayoutAnchor<Axis> {

}

09:31 Now we can return a Constraint function by using the key paths to retrieve the layout anchors from the views:

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

10:20 We can make a second version that uses the same key path for both views, and we can use this version in cases where we want to constrain the same anchor of both views, like their centerXAnchor:

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

11:08 Now we can get rid of the hardcoded equalCenterX and equalCenterY functions and use key paths to achieve the same result:

rootView.addSubview(box, constraints: [
    equal(\.centerXAnchor),
    equal(\.centerYAnchor)
])

11:51 This compiles and works, and we can update a lot of our code to work the same way. To update the track info view's layout, we need another helper method to constrain an anchor to a constant. Constraining to a constant only works for NSLayoutDimension, which is a subclass of NSLayoutAnchor:

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

13:26 Using this method, we can rewrite the layout of the track info view:

rootView.addSubview(trackInfoBox, constraints: [
    equal(\.leftAnchor),
    equal(\.rightAnchor),
    equal(\.heightAnchor, to: trackInfoViewHeight)
])

14:12 This is a lot more declarative than what we had before. And we can use our helper methods to create even more complex constraints, like the button that is constrained to the map view's safeAreaLayoutGuide:

mapView.addSubview(boxedButton, constraints: [
    equal(\.topAnchor, \.safeAreaLayoutGuide.topAnchor),
    equal(\.trailingAnchor, \.safeAreaLayoutGuide.trailingAnchor)
])

15:15 The code isn't necessarily related to views in a Box, so we can actually make it work for any UIView:

extension UIView {
    func addSubview(_ other: UIView, constraints: [Constraint]) {
        addSubview(other)
        other.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate(constraints.map { c in
            c(other, self)
        })
    }
}

extension Box where A: UIView {
    func addSubview<V: UIView>(_ view: Box<V>, constraints: [Constraint]) {
        unbox.addSubview(view.unbox, constraints: constraints)
        references.append(view)
    }
}

16:54 It's possible to use a combination of techniques; we don't have to use the same declarative way of creating constraints everywhere — for example, our track info view uses another constraint that we added manually.

17:09 Our layout code has been cleaned up quite a bit! Let's see if we can improve the app further next week.

Recent Episodes

See All

Unlock Full Access

Subscribe to Swift Talk

  • Watch All Episodes

    New subscriber-only episodes every two weeks

  • Invite Your Team

    Sign up additional team members at 30% discount

  • Support Us

    Ensure the continuous production of new episodes