iOS Auto Layout
Apple's WWDC came and went again. And so it’s time for my review of one of the new toys the guys at Cupertino made for us to play with. I am talking about a feature that Mac OS X has had since 2011, a feature that will now be included in iOS 6. I am talking about Auto Layout.
With iOS 5 and previous operating systems, if we wanted to place a visual element on the screen – like views or buttons – we had to add it manually by setting the element’s frame, that is to say, the element's position and size. Position and size became “magic numbers” we either declared in our code or allowed Interface Builder to set on our xib files.
If we want the layout to be consistent when rotating the screen or when changing view sizes, we manually code what we want, or in simple cases, we use the auto-resizing mask properties. The auto-resizing mask allows us to maintain fairly simple layout options for how things stretch and position themselves when the superview is resized.
With these current options we can get the job done, but if the layout requirements get more advanced, it takes some effort to do it and we end up with code that's hard to understand and maintain.
Understanding Auto Layout
Auto Layout is described as a constraint-based descriptive layout system. This means that you describe what you want and the views figure out what their frames should be, which means you no longer have to use those “magic numbers”.
To describe your views, you set constraints. Each constraint is a linear equation. So if you want a control to be in the top middle of its superview, you can describe that as two constraints:
- Control is a fixed distance from the top of the superview.
- Control is centered horizontally in its superview.
This can be translated to a linear equation such as:
Control.centerX = Superview.centerX Control.top = Superview.top + 10
Furthermore, each formula can be abstracted to:
SomeItem.someAttribute = OtherItem.otherAttribute * multiplier + constant
These are the only elements needed to create a constraint. So, in this example:
Control.centerX = Superview.centerX;
The code will be:
<pre><code> [NSLayoutConstraint constraintWithItem:<redColor>control</redColor> attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:<redColor>superview</redColor> attribute:NSLayoutAttributeCenterX multiplier:1.0 constant:0.0]; </code></pre>
And for this example:
Control.top = Superview.top + 10
The code will be:
<pre><code> [NSLayoutConstraint constraintWithItem:<redColor>control</redColor> attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:<redColor>superview</redColor> attribute:NSLayoutAttributeTop multiplier:1.0 constant:<redColor>10.0</redColor>]; </code></pre>
It's interesting to notice that this API allows a relationship parameter to be defined. In this case, it was NSLayoutRelation
When comparing constraints with auto-resizing masks, you will notice that constraints allow for more control. They’re much more versatile. Constraints can be applied to any two views regardless of view hierarchy and direction. Auto-resizing masks are only from parent to child, while constraints can be from sibling to sibling, from child to parent, from parent to child, and even between cousin views.
In addition to doing this by programming, you can also set constraints in Interface Builder, which allows for a lot of customization. However, you won’t be able to do all you want with Interface Builder alone.
Now, imagine you want to align two buttons together. You may end up with this code:
<pre><code> [NSLayoutConstraint constraintWithItem:<redColor>acceptButton</redColor> attribute:NSLayoutAttribute<redColor>Left</redColor> relatedBy:NSLayoutRelation<redColor>Equals</redColor> toItem:<redColor>cancelButton</redColor> attribute:NSLayoutAttribute<redColor>Right</redColor> multiplier:1.0 constant:<redColor>12</redColor>]; </code></pre>
This code is okay, but it’s a bit confusing – which element ends up to the left and which to the right? Well, Apple also created a visual format syntax capable of dealing with several constraints in a simple way. The following example does the same thing as the previous example:
<pre><code> [NSLayoutConstraint constraintsWithVisualFormat: @”<redColor>[cancelButton]-(12)-[acceptButton]</redColor>” options:0 metrics:nil views:viewsDictionary]; </code></pre>
The viewsDictionary parameter is just a dictionary with the views used on the format string, and options is an alignment mask you can define. Metrics is a dictionary similar to viewsDictionary except that NSNumber replaces the constants used on the format string.
The syntax is powerful and allows a rather big array of options to work with, so I won’t comment on all the options here. I will say, though, that I think the biggest advantage is being able to understand the string with just one glance.
Ambiguity and Satisfying Constraints
The layout system needs two things for Auto Layout to work properly. It needs enough constraints to define position and size for all elements, and it needs constraints not to conflict. If you don’t provide enough constraints, you end up with an undefined layout which might look different with each run. And if constraints conflict, you end up with unsatisfiable constraints and you won’t get the expected result, either.
Even though the system requires size constraints, it’s not always required that they be defined. When the size is missing, the layout system will use the intrinsic size from the UIView’s methods intrinsicContentSize. Different views may override this method in their implementation to define a preferred size.
To deal with conflicts, there is an additional property you can set on constraints that's worth mentioning: priority. Priority tells the layout system which constraint should take priority when two or more constraints are in conflict. This is very interesting because it allows for some subtle yet powerful layout behaviors such as keeping everything aligned but prioritizing the display of content.
Converting to Auto Layout
If you want to change to Auto Layout, you need to understand certain compatibility quirks. For one, you don’t need to jump to Auto Layout. At least, not right now. Auto Layout becomes enabled only when you enable it on your app. If you don’t enable it, you can go on as you are right now.
If you want to convert your whole app, however, it can be done without a problem.
Now, if you want Auto Layout code coexisting with non Auto Layout code, it can be done as well. But there are a couple of things you need to know. If you have a current hard coded layout and you add constraints to a part of that layout, Auto Layout gets enabled automatically. What happens to the rest of the views that don’t have constraints? Remember all views need to have enough constraints to unambiguously define their size and position.
Well, there is property on UIView called translatesAutoresisingMaskIntoConstraints. This is a BOOL and is on by default. It basically adds constraints that are equivalent to the current auto-resizing mask on the view. So all views will behave the way they were before. However, for the views that you are adding by programming constraints, you need to turn this off so the system doesn’t add the auto-resizing mask as constraints that conflict with the constraints you want to add. This is for constraints added in code; Interface Builder handles this for you if you set your constraints there.
So, in conclusion, Auto Layout is more powerful and more meaningful. It simplifies code and makes understanding and maintaining code much easier. A powerful layout tool like this definitely comes in handy when developing apps for devices with new screen sizes, such as the iPhone 5.