Improving Autolayout

posted on

Autolayout has had a lot of bad press. A lot of people find it complex, confusing and more hassle than it's worth. They find the APIs a bit awkward to work with and the tools provided seem to work against them and break what they've done. I'm wanting to change that, so I'm working on various projects to help people learn and use Autolayout.

The Autolayout Guide

From my own experience with Autolayout, and from talking to others about their experience, I'm convinced that 90% of the problems with Autolayout are simply due to people's mindset. Autolayout isn't merely a more powerful form of what we had before, it's a complete conceptual shift. It doesn't help that there isn't much in the way of documentation or guides beyond what Apple provides.

I'm wanting to change that, which is why I have started work on a book called The Autolayout Guide. I want to provide a book that will teach people the conceptual side of Autolayout, the API and tooling side of Autolayout and finally give lots of examples of using Autolayout in real situations. There'll be more information on this as the book progresses. It is still in its early stages at the moment.

M3AppKit

The reason I feel that 90% of the problems people have with Autolayout are due to their mindset, is because I feel the APIs and tools are pretty robust. However, they aren't perfect, which is what the remaining 10% covers. I've got two things I'm looking at to help improve the areas Autolayout is lacking.

The first of these is building an Autolayout Toolkit app, to help in debugging and constructing constraint-based UIs. This is quite a while off yet. My hope is to open source it in the future, but as I'm working on it in spare time, I can't say when it will be released in a usable state, nor what features it will have.

The second is M3AppKit. Over the course of my time writing apps, I've built up a series of useful methods and classes that I've been putting together in frameworks. I've spent the past few weeks tidying up these frameworks, removing anything that isn't essential, adding tests and writing documentation. The result has been the release of M3Foundation 1.0, M3CoreData 1.0, and today M3AppKit 1.0.

While M3AppKit contains many great things, but I want to focus on those that deal with Autolayout. The first is a category on NSLayoutConstraint. Making individual constraints in code can feel awkward, as you have the rather long +constraintWithItem: attribute: relatedBy: toItem: attribute: multiplier: constant: method. This method is great at exposing all the required components of a constraint, but isn't very good at expressing the intent of code.

The most common constraints you will make are size-based constraints or constraints on a view's margin to its superview. NSLayoutConstraint+M3Extensions adds a series of convenience methods to make this simpler and make code easier to read. For example, if you want a constraint to fix a view's width to 100pt, you would previously have had to do this:

[NSLayoutConstraint constraintWithItem:view 
                             attribute:NSLayoutAttributeWidth 
                             relatedBy:NSLayoutRelationEqual 
                                toItem:nil 
                             attribute:NSLayoutAttributeNotAnAttribute 
                            multiplier:1 
                              constant:100];

That is quite a long method call. NSLayoutConstraint+M3Extensions lets you simplify it to this:

[NSLayoutConstraint m3_fixedWidthConstraintWithView:view constant:100];

The other major Autolayout related item is the NSView+M3AutolayoutExtensions category. This adds two methods. The first is simple enough, allowing you to add a subview while also setting its margin constraints like so:

[myView m3_addSubview:mySubview marginsToSuperview:NSEdgeInsetsMake(20, 0, 20, 0)];

That adds the subview and adds constraints to position it 20pt from the top and bottom of the superview and 0pt from the left and right (technically the leading and trailing edges).

The other method requires a bit more explanation, as its aim is to replace all the existing methods of creating constraints with something that is both incredibly flexible and very concise.

The Constraint Equation Syntax

At their most basic, constraints are just representations of equations in the form y = mx + c, your basic linear equation. And Autolayout is merely a linear programming solver, aiming to find the smallest solution to all these constraints.

The existing methods of creating constraints are somewhat awkward. The +constraintWithItem:… method is very verbose and only allows you to specify a particular constraint at a time. The Visual Syntax is more concise and lets you specify a lot of constraints at once, but limits you to working in one axis at a time, and doesn't even support certain layouts.

The Constraint Equation Syntax is my attempt at providing a way to be more concise and more flexible than either of these existing solutions. The best way to demonstrate this is with an example. Below is a basic layout:

Example layout showing three 400 point vertically centred views with equal widths. They each have 20 point margins between themselves and to their parent view, and have top and bottom margins to the parent view of greater than or equal to 20 points

Now this is actually very simple to create in Xcode, but I want to show how we'd go about it in code. First we'll look at the existing methods. I'll try to use the visual syntax as much as possible to reduce the code required. Below is what we'd need to write to achieve this, assumed we'd already stripped away all the constraints in the NIB.

- (void)setupConstraints {
    NSDictionary *views = @{@"view1": self.view1, @"view2": self.view2, @"view3": self.view3};
    
    NSView *view = self.window.contentView;
    
    [view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"|-[view1]-[view2(==view1)]-[view3(==view1)]-|"
                                                                 options:0
                                                                 metrics:nil
                                                                   views:views]];
    
    
    [view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-(>=20)-[view1(==400)]-(>=20)-|"
                                                                 options:0
                                                                 metrics:nil
                                                                   views:views]];
    
    [view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[view2(==400)]"
                                                                 options:0
                                                                 metrics:nil
                                                                   views:views]];
    
    [view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[view3(==400)]"
                                                                 options:0
                                                                 metrics:nil
                                                                   views:views]];
    
    [view addConstraint:[NSLayoutConstraint constraintWithItem:self.view1
                                                     attribute:NSLayoutAttributeCenterY
                                                     relatedBy:NSLayoutRelationEqual
                                                        toItem:view
                                                     attribute:NSLayoutAttributeCenterY
                                                    multiplier:1
                                                      constant:0]];
    
    [view addConstraint:[NSLayoutConstraint constraintWithItem:self.view2
                                                     attribute:NSLayoutAttributeCenterY
                                                     relatedBy:NSLayoutRelationEqual
                                                        toItem:view
                                                     attribute:NSLayoutAttributeCenterY
                                                    multiplier:1
                                                      constant:0]];
    
    [view addConstraint:[NSLayoutConstraint constraintWithItem:self.view3
                                                     attribute:NSLayoutAttributeCenterY
                                                     relatedBy:NSLayoutRelationEqual
                                                        toItem:view
                                                     attribute:NSLayoutAttributeCenterY
                                                    multiplier:1
                                                      constant:0]];
}

With the Equation Syntax we can simplify this to the following:

- (void)setupConstraints {
	NSDictionary *views = @{@"view1": self.view1, @"view2": self.view2, @"view3": self.view3};
	
	[self.window.contentView m3_addConstraintsFromEquations:@[
	 	//20 points between each view
		@"$view1.left = $self.left + 20",
		@"$view2.left = $view1.right + 20",
		@"$view3.left = $view2.right + 20",
		@"$self.right = $view3.right + 20",
		//Equal widths
	 	@"$view2.width = $view1.width",
	 	@"$view3.width = $view1.width",
		//Top and bottom marging of view1 >= 20 (we imply the superview when no other view is given)
		@"$view1.top >= 20",
	 	@"$view1.bottom <= -20" //This constraint technically isn't needed, but is added for completeness
		//All views 400pt tall
		@"$all.height = 400",
		//All views vertically centred
		@"$all.centerY = $self.centerY"
	] substitutionViews:views];
}

For starters you'll notice this is a LOT more concise. If we took out all the comments and blank lines then the Equation Syntax is 15 lines compared to 40 lines for the original. Admittedly I have split up method calls over multiple lines, but even if we sacrificed readability and condensed things as much as possible it's still 4 lines vs 11 lines.

You can reason a bit better about some of the constraints in the Visual Syntax. Margins are always positive, you can visually see how views are laid out in an axis. However, the Equation Syntax displays everything in a consistent and simple manner. The value of the left is equal to the result of the value on the right. You can better reason about what those values could be in your head, and as such better reason about how the constraints you've created will work together.

A great example is the seemingly bizarre $view1.bottom <= -20. This is a result of a convenience shortcut that lets you leave off the other view and attribute when you're constraining a view to its superview. Expanded this really means $view1.bottom <= $self.bottom - 20, and when you start throwing in values you see that this makes sense. If $self.bottom is 100, then the value of $view1.bottom is 80. If we remember the top left is the origin then we see why the constant is -20. Of course if we prefer positive constants, we could just re-write this as $self.bottom >= $view1.bottom + 20 as we have when defining the horizontal margins.

Any confusion with the equations vanishes when you start thinking in terms of the geometry and start inputting theoretical values. Coincidentally this also makes autolayout a lot easier to work with as you're starting to think in its terms of attributes rather than frames.

At the moment all of this stuff is Mac only. I'm hoping to work on iOS versions soon, especially as I want to use this myself on iOS projects. But it's the start of a larger project to try and help others grow to love Autolayout as I do, and be able to explore its full potential.