Auto Layout for iOS Revisited
It has now been four months since I posted Working With Auto Layout on iOS and Rego is now up to version 1.2. It hasn't been perfect and there have been some questions about the performance of auto layout on iOS, but I still believe the benefits outweigh any negatives.
Now that I have a few more months experience using auto layout in Rego I'd like to follow up my original post with some additional thoughts on the technology, how one might use it, and what can, and will, go wrong.
Why Don't We Do It in the Code?
A lot of the negativity towards auto layout is more focused on its integration with Interface Builder than the underlying constraint system. Not only does Interface Builder silently add constraints to keep your layouts valid, it breaks outlets and makes stupid guesses about what you really want. But even though I have my own complaints I think the benefits of working with Storyboards balances out many of these issues. That said, there are times when defining parts of your layout outside of IB is great option.
For example, while most of the main view in Rego is defined in the application's main storyboard file, some of the elements are configured in code. In some cases this is because these elements are positioned similarly on multiple views and updating their constraints in just one place in the code is easier than updating the storyboard in three different places. In others it's simply because the ideal constraint set needed to position them would not be possible using Interface Builder alone. On top of that, with these minor elements positioned in code the storyboard layout is simplified and easier to modify.
Here's the code from Rego that sets up the constraints for the settings and new place buttons on the main view:
- (void)setupLayoutConstraints
{
NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(_addPlaceButton, _settingsButton);
for (UIView *view in [viewsDictionary allValues]) {
view.translatesAutoresizingMaskIntoConstraints = NO;
}
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"[_settingsButton(46)]"
options:0
metrics:nil
views:viewsDictionary]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[_settingsButton(46)]"
options:0
metrics:nil
views:viewsDictionary]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"[_addPlaceButton(46)]"
options:0
metrics:nil
views:viewsDictionary]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[_addPlaceButton(46)]"
options:0
metrics:nil
views:viewsDictionary]];
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:_settingsButton
attribute:NSLayoutAttributeLeft
relatedBy:NSLayoutRelationEqual
toItem:self.mapView
attribute:NSLayoutAttributeLeft
multiplier:1.0
constant:3.0]];
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:_settingsButton
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self.mapView
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:3.0]];
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:_addPlaceButton
attribute:NSLayoutAttributeRight
relatedBy:NSLayoutRelationEqual
toItem:self.mapView
attribute:NSLayoutAttributeRight
multiplier:1.0
constant:-3.0]];
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:_addPlaceButton
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self.mapView
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:3.0]];
}
As you can see, there is one major downside to setting up your constraints in code: There's a lot of typing involved. There isn't even anything clever going on here. I'm just telling auto layout that each button is 46 points tall and 46 points wide and should be positioned 3 points from the top and 3 points from either side of the map view.
This is mostly a matter of taste but I like to set up all my constraints at once in a single method (and then bury that at the bottom of the source file so that I don't have to look at it anymore). When this method is called I first initialize a new dictionary with every view that will be receiving constraints using the NSDictionaryOfVariableBindings
function. Then I loop through all the values in this dictionary and set translatesAutoresizingMaskIntoConstraints
to NO on each view. In this example it doesn't save much typing but I prefer to do as much as possible using the ASCII-art shorthand and then fill in any missing constraints using the addConstraint
method.
Two things to look out for when defining your constraints in code:
- Don't forget to set
translatesAutoresizingMaskIntoConstraints
to NO on every view you're going position in code. - You have to add your views to the view hierarchy before you set their constraints or your won't get very far when you try to run your app.
Defining your auto layout constraints in code can be useful to avoid repetition or simplify your storyboards. In some cases it may be the only option to achieve your desired layout. I believe the trick is in knowing when to work within the limitations of Interface Builder and when to retreat into code.
It's a Trap!
If you do decide to use Interface Builder to set up your auto layout constraints there are a number of pitfalls you need to be aware of. This isn't an exhaustive list by any means, but these are the thing I found either the most frustrating or difficult to diagnose.
Constraints Within Custom Prototype Cells
Prototype table view cells are one of my favorite features that were introduced with storyboards. They really deliver on the Cocoa promise of making simple things simple. If you set up a custom prototype cell using auto layout however, you'll notice that the automatic cell resizing (when editing) is totally broken. This happens because while any views you add to the cell are correctly added as subviews of the cell's content view, the constraints are tied to the cell itself. The net effect of this is that when your content view resizes in response to state changes your subviews will not respond as you probably thought they would. This has to be a bug and hopefully one that will be fixed soon. The only solution I know of is to loop through every subview of your cell and replace the constraints that have the cell as a target with an identical constraint targeting the content view.
Here's the code I'm using (based off of this answer):
- (void)moveConstraintsToContentView
{
for (NSInteger i = self.constraints.count - 1; i >= 0; i--) {
NSLayoutConstraint *constraint = [self.constraints objectAtIndex:i];
id firstItem = constraint.firstItem;
id secondItem = constraint.secondItem;
BOOL shouldMoveToContentView = YES;
if ([firstItem isDescendantOfView:self.contentView]) {
if (![secondItem isDescendantOfView:self.contentView]) {
secondItem = self.contentView;
}
}
else if ([secondItem isDescendantOfView:self.contentView]) {
if (![firstItem isDescendantOfView:self.contentView]) {
firstItem = self.contentView;
}
}
else {
shouldMoveToContentView = NO;
}
if (shouldMoveToContentView) {
[self removeConstraint:constraint];
NSLayoutConstraint *contentViewConstraint = [NSLayoutConstraint
constraintWithItem:firstItem
attribute:constraint.firstAttribute
relatedBy:constraint.relation
toItem:secondItem
attribute:constraint.secondAttribute
multiplier:constraint.multiplier
constant:constraint.constant];
contentViewConstraint.priority = constraint.priority;
[self.contentView addConstraint:contentViewConstraint];
}
}
}
Outlets Will Be Disconnected Without Warning
This one is pretty self explanatory. If you define an outlet for a layout constraint and Interface Builder changes your layout it will happily delete that constraint and leave your outlet unconnected. At least Xcode has these little outlet indicators you can keep an eye on.
Align Baselines Should Die in a Fire
For some reason Interface Builder loves to align baselines. It loves aligning baselines almost as much as it loves deleting your outlet connections. If you position two views horizontally next to each other it will more than likely try to set an align baselines contraint on them. This might not be a big deal but aligning baselines causes weird display issues like squished button images. If you're using auto layout and you can't figure out why your buttons look like crap look for an align baselines constraint.
Up Is Not Always Up
This one is hard to even explain but I'll do my best. When you have a constraint representing a non-zero distance between two views it has a direction. While this direction is not displayed in the inspector, it decides which way the view will move when you modify the constraints constant. So for one constraint type increasing the constant will move it up in relation to the other view and for another it will move it down (or left and right). This is especially fun to discover when working with animations.
Let's End It On A High Note
Auto layout is pretty great. Building Rego for both screen sizes using springs and struts would have been a lot more work and I'm looking forward to speaking with an Xcode engineer at WWDC next month about the improvements they're making in iOS 7.