Autolayout, Equal Spacing and Other Short Stories
posted on
Autolayout is an incredibly powerful API that allows us to build complex and flexible UIs with minimal code. Any layout you can imagine can be defined in Autolayout. However, that is like saying that any program you can imagine can be defined in a Turing complete language. It says nothing to the simplicity or intuitiveness of the solution. So why are some layouts so awkward to produce in Autolayout?
Linear Relationships
Autolayout is built upon linear relationships. These are relationships between one or more variables that can be drawn as a straight line in n-dimensional space. Or put another way, none of the variables in the expression are squared, cubed etc. A linear relationship can be as simple as width = 100
. Or you could make it far more complicated e.g (2 * view1.width) + (3 * view2.width) = view3.width - 50
.
There is technically no limit to the number of variables that can exist in a single expression, though obviously the more variables in an expression the more complex it is to grasp. You can probably tell the intent of the first expression above from a glance, but would have to look at the second one for a while to understand it. The key point is that the maths behind Autolayout allows you, at least in theory, to build incredibly complex relationships. However, Autolayout itself limits what you can do.
Autolayout Constraints
If you cast your mind back to maths classes when you were around 12, you will have encountered the equation of a line in 2D space: y = m * x + c
. That is all constraints are in Autolayout. We can see the similarity if we put it into the same terms as a constraint: view1.attribute = multiplier * view2.attribute + constant
.
The decisions to build the constraints API in this way comes with with advantages and disadvantages. The key advantage is that it dramatically simplifies the API. Every constraint follows the same pattern, which makes it easier to create constraints in code and via a UI. Autolayout already requires a mental shift, making it harder to pick up initially, so making the API more complicated would only exacerbate things.
The downside is that we limit the ways in which we can express a relationship. What may have been one relationship now has to be two or more relationships, and those relationships may require additional views or work on different attributes. The relationships this decisions effects are a small minority of those you'll create. However, one of these problem relationships appears relatively often.
Equal Spacing
The most common example of the disadvantages is when we want to have views that are equally distributed (i.e. the spaces between each of the views are equal in size). This is actually quite simple to do in Autolayout, but requires you to specify spacer views with equal widths, tied to the leading and trailing edges of the views you want spaced out (if you are targeting Mavericks you can use NSStackView to do the same thing in a more elegant way).
But lets imagine that we didn't have the limitation on the number of variables on a constraint, what relationships would we need to define for equal spacing? Lets take a fairly simple example of 4 views equally spaced horizontally in their containing view. The first and last views are both flush against the parent view.
Assuming the widths of the views are fixed, we can define the position of the 4 views as follows. First we have v1 and v4, which are pretty simple as they're flush against the container:
v1.leading = container.leading v4.trailing = container.trailing
For our next two constraints, we need to be able to calculate the size of the space. To do this we need to find the total free space and divide by three, like so:
(container.width - v1.width - v2.width - v3.width - v4.width)/3
To define the position of v2 and v3, we simply set the leading edge to the trailing edge of the previous view plus the space, which gives us the following constraints:
v2.leading = v1.trailing + (container.width - v1.width - v2.width - v3.width - v4.width)/3 v3.leading = v2.trailing + (container.width - v1.width - v2.width - v3.width - v4.width)/3
Autolayout Assembly
Apple could have potentially supported this in Autolayout, and may do in the future. However, there's no way to truly make it elegant using Autolayout in isolation. For example, there could be an API like the one below that allows us to add relationships to various other constraints, but it's still rather ugly. Alternatively we could define it via a more mathematical syntax. Either way they're awkward solutions to what seems like a trivial problem (which coincidentally sums up all of software development).
[NSLayoutConstraint constraintWithItem:v2 attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItems:@[
@{@"item":v1, @"attribute":NSLayoutAttributeTrailing},
@{@"item":container, @"attribute":NSLayoutAttributeWidth, @"multiplier":@(1.0/3)},
@{@"item":v1, @"attribute":NSLayoutAttributeWidth, @"multiplier":@(-1.0/3)},
@{@"item":v2, @"attribute":NSLayoutAttributeWidth, @"multiplier":@(-1.0/3)},
@{@"item":v3, @"attribute":NSLayoutAttributeWidth, @"multiplier":@(-1.0/3)},
@{@"item":v4, @"attribute":NSLayoutAttributeWidth, @"multiplier":@(-1.0/3)},
] constant:0];
Some may consider this as a fundamental flaw in the technology. The thing is, the technology is fine, it just doesn't provide simple solutions to every problem. The simple stuff is simple, the more complex stuff needs abstractions like NSStackView.
In a way, Autolayout is like the Assembly of layout. Assembly allows you to do anything, and if all you're doing is simple stuff (e.g basic maths) then it's pretty easy to work with. It may be a tad verbose or hard to read but it's not difficult. However, when you start getting into more complex problems, you need to start building abstractions like programming languages and compilers which make some of these problems trivial. Over the coming years I expect we'll see more of these abstractions, which will make trivial the few layouts that currently are complex to achieve in Autolayout .