Day 31
Day 31 관련
Project 6, part two
One of the three laws laid down by British science fiction writer Arthur C. Clarke is particularly well known: “any sufficiently advanced technology is indistinguishable from magic.”
It’s possible that you’re thinking Auto Layout is a bit of a black box, where magic happens to make sure all your rules are followed. But it isn’t: Auto Layout is actually straightforward most of the time, and as long as you make sure your constraints are a) complete, and b) non-contradictory, you shouldn’t have too many problems.
Today you have three topics to work through, and you’ll learn about advanced Visual Formatting Language and Auto Layout anchors. Once you’re done, please complete the project review then work through all three of its challenges.
Auto Layout metrics and priorities: constraints(withVisualFormat:)
Auto Layout metrics and priorities: constraints(withVisualFormat:)
We have a working layout now, but it's quite basic: the labels aren't very high, and without a rule regarding the bottom of the last label it's possible the views might be pushed off the bottom edge.
To begin to fix this problem, we're going to add a constraint for the bottom edge saying that the bottom of our last label must be at least 10 points away from the bottom of the view controller's view. We're also going to tell Auto Layout that we want each of the five labels to be 88 points high. Replace the previous vertical constraints with this:
view.addConstraints( NSLayoutConstraint.constraints(withVisualFormat: "V:|[label1(==88)]-[label2(==88)]-[label3(==88)]-[label4(==88)]-[label5(==88)]-(>=10)-|", options: [], metrics: nil, views: viewsDictionary))
The difference here is that we now have numbers inside parentheses: (==88)
for the labels, and (>=10)
for the space to the bottom. Note that when specifying the size of a space, you need to use the - before and after the size: a simple space, -, becomes -(>=10)-
.
We are specifying two kinds of size here: ==
and >=
. The first means "exactly equal" and the second "greater than or equal to." So, our labels will be forced to be an exact size, and we ensure that there's some space at the bottom while also making it flexible – it will definitely be at least 10 points, but could be 100 or more depending on the situation.
Actually, wait a minute. I didn't want 88 points for the label size, I meant 80 points. Go ahead and change all the labels to 80 points high.
Whoa there! It looks like you just received an email from your IT director: he thinks 80 points is a silly size for the labels; they need to be 64 points, because all good sizes are a power of 2.
And now it looks like your designer and IT director are having a fight about the right size. A few punches later, they decide to split the difference and go for a number in the middle: 72. So please go ahead and make the labels all 72 points high.
Bored yet? You ought to be. And yet this is the kind of pixel-pushing it's easy to fall into, particularly if your app is being designed by committee.
Auto Layout has a solution, and it's called metrics. All these calls to constraints(withVisualFormat:)
have been sent nil
for their metrics parameter, but that's about to change. You see, you can give VFL a set of sizes with names, then use those sizes in the VFL rather than hard-coding numbers. For example, we wanted our label height to be 88, so we could create a metrics dictionary like this:
let metrics = ["labelHeight": 88]
Then, whenever we had previously written ==88
, we can now just write labelHeight
. So, change your current vertical constraints to be this:
view.addConstraints( NSLayoutConstraint.constraints(withVisualFormat: "V:|[label1(labelHeight)]-[label2(labelHeight)]-[label3(labelHeight)]-[label4(labelHeight)]-[label5(labelHeight)]->=10-|", options: [], metrics: metrics, views: viewsDictionary))
So when your designer / manager / inner-pedant decides that 88 points is wrong and you want some other number, you can change it in one place to have everything update.
Before we're done, we're going to make one more change that makes the whole user interface much better, because right now it's still imperfect. To be more specific, we're forcing all labels to be a particular height, then adding constraints to the top and bottom. This still works fine in portrait, but in landscape you're unlikely to have enough room to satisfy all the constraints.
With our current configuration, you'll see this message when the app is rotated to landscape: "Unable to simultaneously satisfy constraints." This means your constraints simply don't work given how much screen space there is, and that's where priority comes in. You can give any layout constraint a priority, and Auto Layout will do its best to make it work.
Constraint priority is a value between 1 and 1000, where 1000 means "this is absolutely required" and anything less is optional. By default, all constraints you have are priority 1000, so Auto Layout will fail to find a solution in our current layout. But if we make the height optional – even as high as priority 999 – it means Auto Layout can find a solution to our layout: shrink the labels to make them fit.
It's important to understand that Auto Layout doesn't just discard rules it can't meet – it still does its best to meet them. So in our case, if we make our 88-point height optional, Auto Layout might make them 78 or some other number. That is, it will still do its best to make them as close to 88 as possible. TL;DR: constraints are evaluated from highest priority down to lowest, but all are taken into account.
So, we're going to make the label height have priority 999 (i.e., very important, but not required). But we're also going to make one other change, which is to tell Auto Layout that we want all the labels to have the same height. This is important, because if all of them have optional heights using labelHeight
, Auto Layout might solve the layout by shrinking one label and making another 88.
From its point of view it has at least managed to make some of the labels 88, so it's probably quite pleased with itself, but it makes our user interface look uneven. So, we're going to make the first label use labelHeight
at a priority of 999, then have the other labels adopt the same height as the first label. Here's the new VFL line:
"V:|[label1(labelHeight@999)]-[label2(label1)]-[label3(label1)]-[label4(label1)]-[label5(label1)]->=10-|"
It's the @999
that assigns priority to a given constraint, and using (label1)
for the sizes of the other labels is what tells Auto Layout to make them the same height.
That's it: your Auto Layout configuration is complete, and the app can now be run safely in portrait and landscape.
Auto Layout anchors
Auto Layout anchors
You’ve seen how to create Auto Layout constraints both in Interface Builder and using Visual Format Language, but there’s one more option open to you and it’s often the best choice.
Every UIView
has a set of anchors that define its layouts rules. The most important ones are widthAnchor
, heightAnchor
, topAnchor
, bottomAnchor
, leftAnchor
, rightAnchor
, leadingAnchor
, trailingAnchor
, centerXAnchor
, and centerYAnchor
.
Most of those should be self-explanatory, but it’s worth clarifying the difference between leftAnchor
, rightAnchor
, leadingAnchor
, and trailingAnchor
. For me, left and leading are the same, and right and trailing are the same too. This is because my devices are set to use the English language, which is written and read left to right. However, for right-to-left languages such as Hebrew and Arabic, leading and trailing flip around so that leading is equal to right, and trailing is equal to left.
In practice, this means using leadingAnchor
and trailingAnchor
if you want your user interface to flip around for right to left languages, and leftAnchor
and rightAnchor
for things that should look the same no matter what environment.
The best bit about working with anchors is that they can be created relative to other anchors. That is you can say “this label’s width anchor is equal to the width of its container,” or “this button’s top anchor is equal to the bottom anchor of this other button.”
To demonstrate anchors, comment out your existing Auto Layout VFL code and replace it with this:
for label in [label1, label2, label3, label4, label5] {
label.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
label.heightAnchor.constraint(equalToConstant: 88).isActive = true
}
That loops over each of the five labels, setting them to have the same width as our main view, and to have a height of exactly 88 points.
We haven’t set top anchors, though, so the layout won’t look correct just yet. What we want is for the top anchor for each label to be equal to the bottom anchor of the previous label in the loop. Of course, the first time the loop goes around there is no previous label, so we can model that using optionals:
var previous: UILabel?
The first time the loop goes around that will be nil, but then we’ll set it to the current item in the loop so the next label can refer to it. If previous
is not nil, we’ll set a topAnchor
constraint.
Replace your existing Auto Layout anchors with this:
var previous: UILabel?
for label in [label1, label2, label3, label4, label5] {
label.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
label.heightAnchor.constraint(equalToConstant: 88).isActive = true
if let previous = previous {
// we have a previous label – create a height constraint
label.topAnchor.constraint(equalTo: previous.bottomAnchor, constant: 10).isActive = true
}
// set the previous label to be the current one, for the next loop iteration
previous = label
}
That third anchor combines a different anchor with a constant value to get spacing between the views – these things are really flexible.
Run the app now and you’ll see all the labels space themselves out neatly. I hope you’ll agree that anchors make Auto Layout code really simple to read and write!
Anchors also let us control the safe area nicely. The “safe area” is the space that’s actually visible inside the insets of the iPhone X and other such devices – with their rounded corners, notch and similar. It’s a space that excludes those areas, so labels no longer run underneath the notch or rounded corners.
We can fix that using constraints. In our current code we’re saying “if we have a previous label, make the top anchor of this label equal to the bottom anchor of the previous label plus 10.” But if we add an else
block we can push the first label away from the top of the safe area, so it doesn’t sit under the notch, like this:
if let previous = previous {
// we have a previous label – create a height constraint
label.topAnchor.constraint(equalTo: previous.bottomAnchor, constant: 10).isActive = true
} else {
// this is the first label
label.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 0).isActive = true
}
If you run that code now, you should see all five labels start below the notch in iPhone X-style devices.
Wrap up
Wrap up
There are two types of iOS developer in the world: those who use Auto Layout, and people who like wasting time. It has bit of a steep learning curve (and we didn't even use the hard way of adding constraints!), but it's an extremely expressive way of creating great layouts that adapt themselves automatically to whatever device they find themselves running on – now and in the future.
Most people recommend you do as much as you can inside Interface Builder, and with good reason – you can drag lines about until you're happy, you get an instant preview of how it all looks, and it will warn you if there's a problem (and help you fix it.) But, as you've seen, creating constraints in code is remarkably easy thanks to the Visual Format language and anchors, so you might find yourself mixing them all to get the best results.
Review what you learned
Anyone can sit through a tutorial, but it takes actual work to remember what was taught. It’s my job to make sure you take as much from these tutorials as possible, so I’ve prepared a short review to help you check your learning.
Click here to review what you learned in project 6.
Challenge
One of the best ways to learn is to write your own code as often as possible, so here are three ways you should try extending this app to make sure you fully understand what’s going on:
- Try replacing the
widthAnchor
of our labels withleadingAnchor
andtrailingAnchor
constraints, which more explicitly pin the label to the edges of its parent. - Once you’ve completed the first challenge, try using the
safeAreaLayoutGuide
for those constraints. You can see if this is working by rotating to landscape, because the labels won’t go under the safe area. - Try making the height of your labels equal to 1/5th of the main view, minus 10 for the spacing. This is a hard one, but I’ve included hints below!
Hints
It is vital to your learning that you try the challenges above yourself, and not just for a handful of minutes before you give up.
Every time you try something wrong, you learn that it’s wrong and you’ll remember that it’s wrong. By the time you find the correct solution, you’ll remember it much more thoroughly, while also remembering a lot of the wrong turns you took.
This is what I mean by “there is no learning without struggle”: if something comes easily to you, it can go just as easily. But when you have to really mentally fight for something, it will stick much longer.
But if you’ve already worked hard at the challenges above and are still struggling to implement them, I’m going to write some hints below that should guide you to the correct answer.
If you ignore me and read these hints without having spent at least 31 minutes trying the challenges above, the only person you’re cheating is yourself.
Still here? OK. If you’re stuck on the last challenge, try looking at Xcode’s code completion options for the constraint()
method. We’re using the equalToConstant
option right now, but there are others – the equalTo
option lets you specify another height anchor as its first parameter, along with a multiplier and a constant.
When you use both a multiplier and a constant, the multiplier gets factored in first then the constant. So, if you wanted to make one view half the width of the main view plus 50, you might write something like this:
yourView.widthAnchor.constraint(equalTo: view.safeAreaLayoutGuide.widthAnchor, mul
Wrap up - Additional
If you’re keen to learn more about Auto Layout, I have an Auto Layout cheat sheet that gives you lots of example code to solve common problems.
If you’re feeling mathematically brave, there’s also a great talk by Agnes Vasarhelyi that goes into exactly how the Auto Layout algorithm works behind the scenes – it should prove there’s no magic once and for all!