Quick Anchors are a way of creating programmatic constraints for UIKit with very little code. It's a system that allows you to create UI that looks like this:
With code that looks like this:
view.quickAdd(redView, [
topToTop(50),
leadingToLeading(20),
heightToConstant(100)
])
view.quickAdd(blueView, [
topToTop(redView),
leadingToTrailing(redView, 20),
trailingToTrailing(-20),
bottomToBottom(redView),
widthToRelatedView(redView)
])
view.quickAdd(greenView, [
topToBottom(redView, 20),
leadingToLeading(redView),
trailingToTrailing(blueView),
heightToConstant(50)
])
Programmatic NSLayoutConstraints are a great way of writing clear, easily maintainable UIKit code that will not cause merge conflicts (ahem storyboards ahem). However, writing programmatic UIKit constraints involves a ton of boilerplate. Without Quick Anchors, the layout for the example above would look something like this:
view.addSubview(redView)
NSLayoutConstraint.activate([
redView.topAnchor.constraint(equalTo: view.topAnchor, constant: 50),
redView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
redView.heightAnchor.constraint(equalToConstant: 100)
])
view.addSubview(blueView)
NSLayoutConstraint.activate([
blueView.topAnchor.constraint(equalTo: redView.topAnchor),
blueView.leadingAnchor.constraint(equalTo: redView.trailingAnchor, constant: 20),
blueView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
blueView.bottomAnchor.constraint(equalTo: redView.bottomAnchor),
blueView.widthAnchor.constraint(equalTo: redView.widthAnchor)
])
view.addSubview(greenView)
NSLayoutConstraint.activate([
greenView.topAnchor.constraint(equalTo: redView.bottomAnchor, constant: 20),
greenView.leadingAnchor.constraint(equalTo: redView.leadingAnchor),
greenView.trailingAnchor.constraint(equalTo: blueView.trailingAnchor),
greenView.heightAnchor.constraint(equalToConstant: 50)
])
For this example, Quick Anchors use fewer than half the number of characters than the stock way of creating constraints (441 vs 1,046).
A Quick Anchor is a struct that encapsulates three pieces of information:
- A type of Quick Anchor
- A related view (optional, defaults to the parent view)
- A constant (optional, defaults to 0)
struct QuickAnchor {
let type: QuickAnchorType
let relatedView: UIView? //defaults to parent
let constant: CGFloat? //defaults to 0
}
This is enough information to make a programmatic constraint when adding one view as a subview of another. More on that later.
The QuickAnchorType enum maps to different kinds of NSLayoutConstraints - the way a view should lay itself out in relation to a constant value (eg: height, width) or in relation to another view (eg: spacing, proportional size).
enum QuickAnchorType {
case topToTop
case topToBottom
case topToSafeArea
case heightEqualToView
// etc
}
Not all NSLayoutConstraint types are supported. In order to create a "great than or equal to" constraint, for example, you'll have to use a standard NSLayoutConstraint initializer. For now.
Quick Anchors are exchanged for normal NSLayoutConstraints behind the scenes. Because of this Quick Anchors can easily be used in combination with normal NSLayoutConstraints. Currently there is no clean way to get a reference to a constraint that is added through the Quick Anchor system - if you need access to a constraint reference that constraint needs to be created through the normal NSLayoutConstraint API.
We want to be explicit about all the constraints being added to our views, this is true whether you're using vanilla NSLayoutConstraints or Quick Anchors. For this reason, it's important to set the translatesAutoresizingMaskIntoConstraints property to false for all your views.
let view: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
Quick Anchors extend UIView to add several .quickAdd
functions that are the heart of the Quick Anchor system. .quickAdd
functions do two things in a single step:
- Add one view as a subview of another
- Translate any Quick Anchors passed in into constraints
For example, say you have two views, redView
and blueView
.
let redView: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .red
return v
}()
let blueView: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .blue
return v
}()
..and say you want to add redView as a subview of blueView, centering redView vertically and horizontally, and giving it an explicit height of 100 and an explicit width of 100.
If you wanted to accomplish this with programmatic NSLayoutConstraints, your code would look something like this:
// NSLayoutConstraint
blueView.addSubview(redView)
NSLayoutConstraint.activate([
redView.centerXAnchor.constraint(equalTo: blueView.centerXAnchor),
redView.centerYAnchor.constraint(equalTo: blueView.centerYAnchor),
redView.heightAnchor.constraint(equalToConstant: 100),
redView.widthAnchor.constraint(equalToConstant: 100)
])
Not too bad, but there is a lot of repeated code. Let's see how this could look with Quick Anchors:
// Quick Anchors - Option 1
blueView.quickAdd(redView, [
QuickAnchor.init(.centerX),
QuickAnchor.init(.centerY),
QuickAnchor.init(.heightEqualToConstant, 100),
QuickAnchor.init(.widthEqualToConstant, 100)
])
Much less code! The first example had 313 characters, but with Quick Anchors its reduced to 190.
But you can take things even father using available Quick Anchor global initializers.
// Quick Anchors = Option 2
blueView.quickAdd(redView, [
centerX(),
centerY(),
heightToConstant(100),
widthToConstant(100)
])
108 characters total. The resulting code is so compact, it can easily be parsed if it's written on a single line:
blueView.quickAdd(redView, [ centerX(), centerY(), heightToConstant(100), widthToConstant(100) ])
It's important to stress that quick anchors are exchanged for normal NSLayoutConstraints behind the scenes and can therefore be used interchangably with them. This can be important if you would like to use a constraint that Quick Anchors do not support, or if you need to assign a constraint to a variable so that it can be modified later.
For example, code like this is perfectly valid:
blueView.quickAdd(redView, [
centerX(), centerY()
])
redView.heightAnchor.constraint(lessThanOrEqualToConstant: 100).isActive = true
redView.widthAnchor.constraint(greaterThanOrEqualTo: blueView.widthAnchor).isActive = true
As is code like this:
blueView.quickAdd(redView, [
centerX(), centerY(), widthToConstant(100)
])
var c = redView.heightAnchor.constraint(equalToConstant: 100)
c.isActive = true
if someVariable == true {
c.constant = 200
}
It's very common to add one view as a subview of another and set constant padding all around its edges. Instead of having to be explicit about the quick anchors:
blueView.quickAdd(redView, [
.topToTop(10), .leadingToLeading(10), .trailingToTrailing(-10), .bottomToBottom(-10)
])
You can instead use an overloaded version of quickAdd
which allows you to pass a single Int to use for the padding of all 4 sides.
blueView.quickAdd(redView, 10)
And if want 0 values along all sides, you may simply omit the Int parameter:
blueView.quickAdd(redView)
For additional brevity, there is another version of quickAdd
which accepts an array of Int?
s, allowing you to supply the paddings you would like to use for the top, left, bottom, and right of a view in relation to its superview. This array must be 4 items long, otherwise a FatalException will be thrown. Pass nil for any edges you would not like padding added to.
blueView.quickAdd(redView, [10, 20, nil, -20])
Because the parameters are not named, they must be passed in the correct order. The order top, left, bottom, and right was chosen because it matches the order when creating a UIEdgeInsets instance.
Sometimes you might want to use the "array of Ints" quickAdd
function, but instead of giving the padding in relation to the superview, you would like to express the view's padding in relation to another view. To do this, you may simply replace any or all of the Int values passed into quickAdd
with a (UIView, Int) tuple instead, where UIView is the view that you would like to create your constraint in relation to.
Same stipulations as the previous example still apply. The array's length must be 4, otherwise a fatal exception will be thrown.
This is incredibly useful if you're stacking views along the Y axis.
blueView.quickAdd(redView, [10, 20, -20, nil])
blueView.quickAdd(greenView, [(redView, 10), 20, -20, -20])
blueView.quickAdd(yellowView, [(green, 40), 20, -20, -20])
Finally, if you would like to supply top + left + bottom + right paddings AND also supply quick anchors, you may also supply an array of QuickAnchors to the overloaded quickAdd
function. This array can be any length.
blueView.quickAdd(redView,
[10, 30, 30, nil],
[ heightToConstant(100) ]
)
Whenever possible, it is recommended to use convenience initializers for UIView subclasses in order to further increase brevity.
Good convenience initialzers will let you take code like this:
let b = UIButton()
b.translatesAutoresizingMaskIntoConstraints = false
b.setTitle("Press Me", for: .normal)
b.addTarget(self, action: #selector(handleButtonPress), for: .touchUpInside)
view.quickAdd(b, 10)
and reduce it down to code like this:
let b = UIButton(title: "Press Me", target: self, action: #selector(handleButtonPress))
view.quickAdd(b, 10)
Be smart about your convenience initializers. If you find yourself always creating UIViews and then setting a different background and corner radius, create a specialized initializer.
To get you started, I have created a gist of my favorite UIView subclass convenience initializers.
There are many ways to write the following quickAdd
function:
blueView.quickAdd(redView, [
centerX(),
centerY(),
heightToConstant(100),
widthToConstant(100)
])
The above function reads very nicely, but takes up a lot of vertical space. It could be written much more compactly in this way:
blueView.quickAdd(redView, [
centerX(), centerY(), heightToConstant(100), widthToConstant(100)
])
But the preferred way to write this line would be:
blueView.quickAdd(redView, [ centerX(), centerY(), heightToConstant(100), widthToConstant(100) ])
This allows you to group all layout code together in an extremely compact area of your class.
Obviously, the following example contains some repeated values:
blueView.quickAdd(redView, [
topToTop(40), leadingToLeading(20), trailingToTrailing(-20)
])
blueView.quickAdd(greenView, [
topToBottom(redView, 40), leadingToLeading(20), bottomToBottom(-40), trailingToTrailing(-20)
])
It might be tempting to refactor the repeated values out into variables, like this:
let topBottomPadding = 40
let leftRightPadding = 20
blueView.quickAdd(redView, [
topToTop(topBottomPadding), leadingToLeading(leftRightPadding), trailingToTrailing(-leftRightPadding)
])
blueView.quickAdd(greenView, [
topToBottom(redView, topBottomPadding), leadingToLeading(leftRightPadding), bottomToBottom(-topBottomPadding), trailingToTrailing(-leftRightPadding)
])
This is obviously helpful in some ways. If the paddings ever change you only have to make the update in a single place, code complete will prevent you from accidentally typing the wrong value, etc.
However, the resulting reduction in brevity usually makes this kind of refactor not worthwhile and should usually be avoided.