Apple introduced iOS support for Auto Layout in iOS 6.0, and while it wasn’t love at first sight, I eventually came to appreciate the power and flexibility of NSLayoutConstraint. I’m one of those developers who doesn’t use Interface Builder, nibs, or Storyboards. I’m not against any of those technologies…it’s just that I like to build my interfaces in code. For me, Auto Layout has made it much easier to create Universal Apps that intelligently resize to take advantage of multiple display types.
Recently, I’ve spent a lot of time updating my apps for iOS 7, and along the way, I’ve added Auto Layout to my older apps. During this process, I found myself writing verbose Auto Layout code over and over. For example, to center a view both horizontally and vertically in its superview, you do something like this:
[self.view addConstraint:
[NSLayoutConstraint constraintWithItem:view1
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:view1.superview
attribute:NSLayoutAttributeCenterX
multiplier:1.0f
constant:0.0f]]
[self.view addConstraint
[NSLayoutConstraint constraintWithItem:view1
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:view1.superview
attribute:NSLayoutAttributeCenterY
multiplier:1.0f
constant:0.0f]];
This tells Auto Layout that you’d like the centerX and centerY attributes of view1 to visually match the same attributes of its superview (the coordinates are automatically transformed between views). Thankfully, Xcode’s code completion feature makes these statements relatively easy to write.
Auto Layout also supports an ASCII art-like visual format that can be used to create a set of constraints. To position a view 15 points from the left edge of its superview:
[self.view addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-15-[view1]"
options:0
metrics:nil
views:@{ @"view1" : view1 }]];
The views parameter is an NSDictionary that maps the string name of each view to the view itself. You can also pass variable metrics values using the metrics parameter.
To position the view 15 points from the top edge of its superview, you’d add:
[self.view addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-15-[view1]"
options:0
metrics:nil
views:@{ @"view1" : view1 }]];
If you look around, you’ll find categories and helper libraries for NSLayoutConstraint that make many of these common situations much easier. But a lot of them are also focused on adding unique methods to handle each situation. I wanted something that would allow me to easily express both of Auto Layout’s constraint creation methods, ideally in a single call, while taking full advantage of the metrics and views dictionaries that I’m already creating.
So, like any developer, I decided to roll my own (developer trope, anyone?).
JBNSLayoutConstraint
First, I decided that my category should accept multiple visually-formatted constraints separated by a semicolon. So, the previous two statements collapse to:
[self.view addConstraints:
[NSLayoutConstraint
jb_constraintsWithVisualFormat:@"H:|-15-[view1];V:|-15-[view1]"
options:0
metrics:nil
views:@{ @"view1" : view1 }]];
I’ve noticed that it’s very common for me to pass the same views dictionary into both a horizontal and vertical visual format string, and this just makes it easier to do in a single statement.
Second, if you look at Apple’s NSLayoutConstraint Class Reference, you’ll see them explain that the basic form of a single constraint looks a lot like a linear equation:
view1.attr1 <relation> view2.attr2 * multiplier + constant
So, I reasoned, if I’m already passing in dictionaries of metrics and views, why don’t I just add a new visual format for these expressions? Here’s my string-based version of the first two examples from this post:
[self.view addConstraints:
[NSLayoutConstraint
jb_constraintsWithVisualFormat:@"view1.centerX==|.centerX;view1.centerY==|.centerY"
options:0
metrics:nil
views:@{ @"view1" : view1 }]];
That’s more like it. These are the equations that I have in my head when I’m thinking about constraints anyway. Note that the pipe (|) character—like in Auto Layout’s existing visual format—continues to refer to a view’s superview.
Even better, if the two attributes in an expression are identical, you can omit the second attribute and end up with this:
[self.view addConstraints:
[NSLayoutConstraint
jb_constraintsWithVisualFormat:@"view1.centerX==|;view1.centerY==|"
options:0
metrics:nil
views:@{ @"view1" : view1 }]];
But even that seemed like too much to type (remember, developers are lazy), so I created compound attributes to address a few common cases. For example, the center compound attribute handles both the centerX and centerY constraints, making this statement functionally equivalent to the last:
[self.view addConstraints:
[NSLayoutConstraint jb_constraintsWithVisualFormat:@"view1.center==|"
options:0
metrics:nil
views:@{ @"view1" : view1 }]];
Another compound attribute is size:
[self.view addConstraints:
[NSLayoutConstraint jb_constraintsWithVisualFormat:@"view1.size==50.0"
options:0
metrics:nil
views:@{ @"view1" : view1 }]];
To add a priority, just like you do for the existing visual format, include it after an at sign:
[self.view addConstraints:
[NSLayoutConstraint jb_constraintsWithVisualFormat:@"view1.size==50.0@750"
options:0
metrics:nil
views:@{ @"view1" : view1 }]];
Constraint Installation
As a habit, I have always tried to add constraints to the nearest common ancestor of their referenced views. But, because I’m lazy (see how I foreshadowed that?), I sometimes find it easier to just add them to a common view up in the hierarchy.
I ran across Florian Kugler’s post, Auto Layout Performance on iOS, and Martin Pilkington’s follow-up, Optimising Autolayout, earlier this year, and those posts inspired me to create a second category that adds jb_install and jb_uninstall methods to NSLayoutConstraint.
As you’d expect, jb_install finds the nearest common ancestor of a constraint’s referenced views and installs itself. It also sets translatesAutoresizingMaskIntoConstraints to NO for the referenced views. jb_uninstall is self-explanatory.
In addition to methods that also install and uninstall an array of constraints, the One Method to Rule Them All does all of the good stuff at once:
[NSLayoutConstraint
jb_installConstraintsWithVisualFormat:@"view1.center==|"
options:0
metrics:nil
views:@{ @"view1" : view1 }]];
This is the method that I use most frequently.
Implementation
I briefly considered using a formal parser generated by something like ANTLR for evaluating my new expression format, but after some research, I realized that it would be overkill (and overweight) for my relatively simple needs. So, I wrote a straightforward implementation that is smart enough to catch most of the common formatting errors. I wouldn’t call it elegant, but it works.
I hope this makes your use of Auto Layout and NSLayoutConstraints just a little bit easier!
Enough talking…take me to the JBNSLayoutConstraint project page!
Leave a Reply