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.