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:

  1. Don't forget to set translatesAutoresizingMaskIntoConstraints to NO on every view you're going position in code.
  2. 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.

"Auto Layout for iOS Revisited" was originally published on 18 May 2013.