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. 🤓