As you know, iOS gives us a few different ways to create our user interfaces. Here at SeatGeek, we build our views in code. We’ve found that using auto layout together with the Masonry library makes it pretty easy to write our layout code, and pretty easy to maintain it later. Readability aside, though, auto layout does come with some complexities and gotchas that can be frustrating at first. This blog post is a list of some stuff we wish we’d known when we started.
Helper libraries:
- Masonry provides a nice DSL for creating auto layout constraints in code
- SnapKit is the Swift equivalent, pretty much exactly the same thing
UIView subclasses:
- For custom UIView subclasses that use auto layout, usually we need to override
+requiresConstraintBasedLayout
to return true. If you do everything perfectly and see an empty screen, it could be because this is missing.
Intrinsic content size:
- The native
UIView
subclasses provided in iOS, likeUILabel
orUIImageView
, usually know how big they should be, so you often need only specify position when laying them out. - Our own custom
UIView
subclasses can also have intrinsic content size, if we set up constraints to drive it. For example, it’s pretty common to drive intrinsic height by relating the top subview’s top edge to the view’s top edge, the bottoms and tops of the succeeding views to each other as we go down, and then the bottom subview’s bottom edge to the view’s bottom edge. - Or, our views can have their content size driven by the containing view’s layout. For example, it’s common for the container to control the width, since we don’t usually have horizontal scrolling. We often still have the widths of the subviews fully related as in the height example above, but in this case, rather than the subviews driving the instrinsic width of their container, the container drives the widths of the subviews.
- It’s very common to have both approaches: the container drives the width from outside, but the subviews drive the height (or the
contentSize.height
, for a scroll view, see below) from the inside. - Using intrinsic content size results in a lot less layout code — often you’ll see only relative / absolute positioning to locate the subviews. Really, subview size is often an internal detail, and it’s nice not to be hard coding it everywhere a reusable view is used.
- Of course, we do often constrain subview sizes, especially widths. For example with a
UILabel
, I’d often set the width, but use its intrinsic height based on the font. - As discussed above, custom
UIView
subclasses we make can also have intrinsic size. We get this for free if we use auto layout. Otherwise we can override-intrinsicContentSize
to make it easy for auto layout clients.
Scroll views:
- For scroll views, we usually want the container to specify the frame, so we set up constraints that do that. Then we want the
contentSize.width
to be determined by the container (usually the same as the frame width), but we want the scroll view’s contents to drive the height. contentSize.height
is easy — just make sure that you have constraints as discussed above to determine the height. Typically the intrinsic heights of the subviews together with vertical spacing constraints to the scroll view do it.contentSize.width
usually requires a trick. If you just relate the scroll view width to its container, that sets the scroll view’s frame, not the content size width. The solution I’ve usually used is to constraincontentSize.width
by relating a subview of the scroll view to the containing view. If there is no suitable subview handy, I’ll add a dummy view only for that purpose.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Content compression resistance and hugging:
- Sometimes we need to give auto layout a little more information to know which fields are OK to stretch beyond their instrinsic size, and which ones are not. For example: you have an image with a label to the right of it. The left of the image is related to the left of the container, the right to the left of the label, and the right of the label to the right of the container. You want the image to be its natural size, and the label to stretch to the full available width, even if the text is currently shorter. If iOS stretches the image horizontally instead of the label, you can add this code:
1
|
|
Multiline labels:
- Unfortunately, multiline labels sometimes don’t behave the way you’d expect unless you set
preferredMaxLayoutWidth
. The problem with setting that width at constraint creation time is that you give up dynamic layout update if the container width changes. One solution is to use aUILabel
subclass containing a small fix. There’s been such a subclass at the last 3 places I’ve worked:
1 2 3 4 5 6 7 8 |
|
Centering groups of views:
- To center groups of views, normally we wrap them in a container view and center that.
Changing layout later on:
- Since auto layout is a bit more flexible than autoresizing masks, often an initial set of constraints is all you ever need. If the view’s bounds are changed, iOS will automatically update the layout using the existing constraints. In these cases, you can create the constraints just one time in
-viewDidLoad
or aUIView
initializer. - Every once in a while, you may need different constraints when the view is in different states. A common example is when a view that affects the layout of other views is hidden or shown. In that case, you can create the constraints in the
-viewDidUpdateConstraints
or-updateConstraints
methods. When a state change that affects the constraints occurs, just call-setNeedsUpdateConstraints
on the view. However, this means your constraint creation code will be run multiple times, so it needs to be idempotent —- see the next section.
Writing idempotent constraint code:
- Whenever our constraint creation code may be run more than once, we need to be careful not to add on a new set of duplicate constraints each time, some of which may be in conflict, spew warnings to the console, and cause your layout to be chaotic as iOS breaks constraints randomly to get a set it can satisfy.
- First, never use
-mas_makeConstraints
in code that may run more than once. Use either-mas_updateConstraints
(if you always make the same constraints but their constant values change), or-mas_remakeConstraints
(if you make different constraints for different states). Very rarely as a last resort, you might need to save the objects returned by these Masonry methods and invoke-uninstall
on each. - You might wonder why we need to use those different Masonry methods. Why can’t we can’t just clear all the constraints out of our view before adding them again with
-mas_makeConstraints
? It turns out this is tricky, because some of the constraints that you manage in-viewDidUpdateConstraints
or-updateConstraints
are actually owned by subviews. It’s not easy to remove just the constraints you’re about to re-add. Some constraints that your containing view manages (which you should NOT remove) may actually be owned by your view. Similarly, some constraints that your view manages (which you should remove) may actually be owned by subviews. You only want to clear the constraints that your view manages, that you’re going to add again. Masonry provided the above methods to make that easy. - Couple gotchas with the UIViewController
-viewDidUpdateConstraints
method: (1) it’s often called more than once even when you didn’t call-setNeedsUpdateConstraints
, and (2) sometimes you need to set up your constraints earlier in-viewDidLoad
for everything to work nice. Because of this, I got in the habit of always making my constraints in a helper method-addOrUpdateConstraints
, and always making them idempotent as above. I call the helper from-viewDidLoad
, and add a call from-viewDidUpdateConstraints
if necessary. That also keeps-viewDidLoad
from getting too cluttered up. - Gotcha with the UIView
-updateConstraints
method: the super call must be made, and is supposed to go at the end.
Animations:
- When you invoke
-setNeedsUpdateConstraints
on a view, or update a constant value inside a constraint directly, iOS will mark the view as needing layout, and set new frames using the updated constraints when it gets around to the next layout pass. The frames are not updated synchronously. If you need frames to update at a certain time, for example within an animation block, invoke-layoutIfNeeded
on the view. Constraints are not animated, frames are, just like always.
Gotchas:
- Sometimes table header views behave strangely if auto layout is used, on iOS 7 & 8 at least. For those cases and other rare times where auto layout just doesn’t seem to work right, I usually time box it and fall back to another method like programatically setting frames in
-layoutSubviews
/-viewDidLayoutSubviews
.
Let us Know
We hope some of these tips prove helpful. We’re curious what useful techniques you’ve discovered, so feel free to let us know in the comments!
Help Us Take it to the Next Level
If you think this kind of stuff is interesting, consider working with us on the iOS team at SeatGeek. Or, if you’re not an iOS Engineer, we have other openings as well!