Using Auto Layout for iOS
Auto Layout is a powerful system for iOS that lets you write flexible UIs for any device and orientation without having to deal with the frame of the views. You won't need to do math with CGRects and CGPoints anymore.
To explain how it works, let's create a sample app for this simple screen:
We can see the following UI elements that are present:
- UIImageView for the group picture
- UIImageView for the logo
- UILabel for the company name
- UIView with white background color as the container for the logo and name
Before Auto Layout we would look at this screen and say that:
- group picture size is 568x320 and is located at position (0,0)
- container size is 150x56 and located at position (115,264)
- logo size is 44x44 and located inside container at position (6,6)
- label size is 88x25 and located inside container at position (56,21)
But with Auto Layout we can start to think how the views are related to each other and not care about frame sizes and points. Analyzing the screen, we can see the following rules:
- group picture has the same size as the screen
- container view is at the bottom of the screen
- container view is centered on x-axis
- width of container view is margin of 6px + width of logo + margin of 6px + width of label + margin of 6px
- height of container view is margin of 6px + height of logo + margin of 6px
- logo and label are centered on y-axis
Now it's time to implement the screen and make the code that understands these rules. The first step is to create a controller that contains a UIImageView to display the group picture:
@interface RootViewController () {
UIImageView *_backgroundView;
}
@end
@implementation RootViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self.view addSubview:self.backgroundView];
}
-(UIImageView *)backgroundView {
if (!_backgroundView) {
_backgroundView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"group-picture"]];
[_backgroundView setContentMode:UIViewContentModeScaleAspectFill];
}
return _backgroundView;
}
@end
Note that we didn't set the frame of the UIImageView since we will be using Auto Layout.
Now let's create the first constraints to satisfy rule 1 "group picture has the same size as the screen":
- (void)viewDidLoad {
...
[self addConstraints];
}
-(UIImageView *)backgroundView {
...
[_backgroundView setTranslatesAutoresizingMaskIntoConstraints:NO];
}
-(void)addConstraints {
id views = @{@"background": self.backgroundView};
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[background]|" options:0 metrics:nil views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[background]|" options:0 metrics:nil views:views]];
}
Inside viewDidLoad we call our new method addConstraints that contains 2 constraints. On this example, we are using the visual format language to define the constraints.
The first one is "H:|[background]|":
- "H:" means that this constraint is for the horizontal layout, the x-axis
- "|" is a reference to the view which the constraints are being added. In our case is the controller's view.
- "[background]" is a reference to the backgroundView
By default, the controller's view size is the same size of the screen. So when we add the "[background]" surrounded by "|", and prepended with "H:", we are saying that the width of the backgroundView will be the same width of the screen. The other constraint "V:|[background]|" is the same idea but for the vertical layout, and defines the height of the backgroundView.
Another thing to mention is that we had to call setTranslatesAutoresizingMaskIntoConstraints:NO in the backgroundView. iOS 6 introduced Auto Layout and before it, if we wanted to create flexible views we would have to use autoresizing masks. To keep compatibility with previous iOS versions, all autoresizing masks are translated into constraints by default. If you want to use Auto Layout you have to disable this in all views that are referenced in the constraints otherwise you will get some exceptions related to conflicting constraints.
Now it's time to create the container view. We could do everything inside the controller, but in this case it's better to extract all the elements to a custom subview. So we create a subclass of UIView called LogoView that contains the logo and the label.
@interface LogoView () {
UIImageView *_imageView;
UILabel *_label;
}
@end
@implementation LogoView
- (id)init {
self = [super init];
if (self) {
[self setBackgroundColor:[UIColor colorWithWhite:1 alpha:0.8]];
[self addSubview:self.imageView];
[self addSubview:self.label];
}
return self;
}
-(UIImageView *)imageView {
if (!_imageView) {
_imageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"hr_logo_rocket"]];
[_imageView setTranslatesAutoresizingMaskIntoConstraints:NO];
}
return _imageView;
}
-(UILabel *)label {
if (!_label) {
_label = [[UILabel alloc] init];
[_label setText:@"Hashrocket"];
[_label setTranslatesAutoresizingMaskIntoConstraints:NO];
}
return _label;
}
To satisfy the rules 4, 5 and 6 we just need 2 visual constraints:
-(void)addConstraints {
id views = @{@"image": self.imageView, @"label": self.label};
id metrics = @{@"margin": @6};
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-margin-[image]-margin-[label]-margin-|" options:NSLayoutFormatAlignAllCenterY metrics:metrics views:views]];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-margin-[image]-margin-|" options:0 metrics:metrics views:views]];
}
To explain the first constraint we have to understand how UIImage and UILabel define their size. By default, the width and height of the UIImageView are the actual width and height of the .png file. The UILabel width and height are calculated by the font size and text length.
Now that we know this, we can use the visual format and put both views side by side and separate them with some margin that comes from a metrics option. The pipes surrounding these visual constraints ensure everything is laid out properly.
What we have to understand here is that the pipes in this example behave different than the first example. In both examples, the pipes represent the superview of the views we're laying out. Where things differ is that in the first example, anchoring our visual constraint with the pipes cause the subview to set it's width equal to the superview, while in this example the effect is to cause the superview's size to change based on the subview's size.
This is really cool! If later we want to update the label text to "Hashrocket 2013", the LogoView will automatically change it's size, because the UILabel increased it's width.
Also note that we passed the option NSLayoutFormatAlignAllCenterY to the first constraint. This option makes both views to be aligned on the y-axis. This satisfies rule 6.
The second constraint defines the height of the LogoView. Since the image height is greater than the label height, we can use the same idea as the previous constraint and only use the "[image]" height to define the LogoView height.
Now it's time to add this custom view to the RootViewController and position it on the screen to satisfy rules 2 and 3:
- (void)viewDidLoad {
...
[self.view addSubview:self.logoView];
...
}
-(LogoView *)logoView {
if (!_logoView) {
_logoView = [[LogoView alloc] init];
[_logoView setTranslatesAutoresizingMaskIntoConstraints:NO];
}
return _logoView;
}
-(void)addConstraints {
id views = @{@"logo": self.logoView, ...};
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[logo]|" options:0 metrics:nil views:views]];
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.logoView attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeCenterX multiplier:1 constant:0]];
...
}
The first constraint "V:[logo]|" uses only one pipe at the end. This means that the bottom of the logoView is at the bottom of the controller's view.
To satisfy rule 3 "container view is centered on x-axis" we can't use the visual format. We have to create a constraint based on attributes. The constraint we created says the logoView's centerX attribute is equal to the controller view's centerX attribute.
The visual format is really cool and easy to read, but it's not enough to describe more complex relationships between views. It is worth noting that any constraint created using the visual format could be created using constraintWithItem.
For instance, the visual format constraint “V:[logo]|” could be rewritten to use constraintWithItem, like this:
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.logoView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeBottom multiplier:1 constant:0]];
But now the code starts to become more verbose and not so easy to read as the visual format. That's when the library Masonry comes in. It's a DSL that makes the attribute constraints succinct. So the same example we could write as:
[self.logoView mas_makeConstraints:^(MASConstraintMaker *make) {
make.bottom.equalTo(self.view);
make.centerX.equalTo(self.view);
}];
And if we refactor the addConstraints method to also use Masonry to layout the backgroundView, we would finally have something like this:
-(void)addConstraints {
[self.backgroundView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self.view);
}];
[self.logoView mas_makeConstraints:^(MASConstraintMaker *make) {
make.bottom.equalTo(self.view);
make.centerX.equalTo(self.view);
}];
}
Much nicer!
This sample app is available on github if you want to run it.
Auto Layout is a great way to write flexible and maintainable views dependencies. In some cases it's better to use the visual format, but when it's not enough you can go straight to Masonry and write beautiful constraints!
Update: Also check out this other blog post where we introduce our new library ConstraintFormatter that unify visual constraints and constraints based on attributes.