Auto Layout: Programmatic Constraints - iOS Programming: The Big Nerd Ranch Guide (2014)

iOS Programming: The Big Nerd Ranch Guide (2014)

16. Auto Layout: Programmatic Constraints

In this chapter, you are going to interact with Auto Layout in code. Apple recommends that you create and constrain your views in a XIB file whenever possible. However, if your views are created in code, then you will need to constrain them programmatically.

To have a view to work with, you are going to recreate the image view programmatically and then constrain it in the UIViewController method viewDidLoad. This method will be called after the NIB file for BNRDetailViewController’s interface has been loaded.

Recall that in Chapter 6, you overrode loadView to create views programmatically. If you are creating and constraining an entire view hierarchy, then you override loadView. If you are creating and constraining an additional view to add to a view hierarchy that was created by loading a NIB file, then you override viewDidLoad instead.

In BNRDetailViewController.m, implement viewDidLoad to create an instance of UIImageView.

- (void)viewDidLoad

{

[super viewDidLoad];

UIImageView *iv = [[UIImageView alloc] initWithImage:nil];

// The contentMode of the image view in the XIB was Aspect Fit:

iv.contentMode = UIViewContentModeScaleAspectFit;

// Do not produce a translated constraint for this view

iv.translatesAutoresizingMaskIntoConstraints = NO;

// The image view was a subview of the view

[self.view addSubview:iv];

// The image view was pointed to by the imageView property

self.imageView = iv;

}

The line of code regarding translating constraints has to do with an older system for scaling interfaces – autoresizing masks. Before Auto Layout was introduced, iOS applications used autoresizing masks to allow views to scale for different-sized screens at runtime.

Every view has an autoresizing mask. By default, iOS creates constraints that match the autoresizing mask and adds them to the view. These translated constraints will often conflict with explicit constraints in the layout and cause an unsatisfiable constraints problem. The fix is to turn off this default translation by setting the property translatesAutoresizingMaskIntoConstraints to NO. (There is more about Auto Layout and autoresizing masks at the end of this chapter.)

Now let’s consider how you want the image view to appear. The UIImageView should span the entire width of the screen and should maintain the standard 8 point spacing between itself and the dateLabel above and the toolbar below. Here are the constraints for the image view spelled out:

· left edge is 0 points from the image view’s container

· right edge is 0 points from the image view’s container

· top edge is 8 points from the date label

· bottom edge is 8 points from the toolbar

Apple recommends using a special syntax called Visual Format Language (VFL) to create constraints programmatically. This is how you will constrain the image view. However, there are times when a constraint cannot be described using VFL. In those cases, you must take another approach. You will see how to do that at the end of the chapter.

Visual Format Language

Visual Format Language is a way of describing constraints in a literal string. You can describe multiple constraints in one visual format string. A single visual format string, however, cannot describe both vertical and horizontal constraints. Thus, for the image view, you are going to come up with two visual format strings: one that constrains the horizontal spacing of the image view and one that constrains its vertical spacing.

Here is how you would describe the horizontal spacing constraints for the image view as a visual format string:

@"H:|-0-[imageView]-0-|"

The H: specifies that these constraints refer to horizontal spacing. The view is identified inside square brackets. The pipe character (|) stands for the view’s container. This image view, then, will be 0 points away from its container on its left and right edges.

When the number of points between the view and its container (or some other view) is 0, the dashes and the 0 can be left out of the string:

@"H:|[imageView]|"

The string for the vertical constraints looks like this:

@"V:[dateLabel]-8-[imageView]-8-[toolbar]"

Notice that “top” and “bottom” are mapped to “left” and “right”, respectively, in this necessarily horizontal display of vertical spacing. The image view is 8 points from the date label at its top edge and 8 points from the toolbar at its bottom edge.

You could write this same string like this:

@"V:[dateLabel]-[imageView]-[toolbar]"

The dash by itself sets the spacing to the standard number of points between views, which is 8.

To see a little more of VFL grammar, consider a hypothetical situation. Imagine you had two image views with the following horizontal constraints:

· the horizontal spacing between the image views should be 10 points

· the lefthand image view’s left edge should be 20 points from its superview

· the righthand image view’s right edge should be 20 points from its superview

You could describe the three constraints in one visual format string:

@"H:|-20-[imageViewLeft]-10-[imageViewRight]-20-|"

The syntax for a fixed size constraint is simply adding an equality operator and a value in parentheses inside a view’s visual format:

@"V:[someView(==50)]"

This view’s height would be constrained to 50 points.

Creating Constraints

A constraint is an instance of the class NSLayoutConstraint. When creating constraints programmatically, you explicitly create one or more instances of NSLayoutConstraint and then add them to the appropriate view object. Creating and adding constraints is one step when working with a XIB, but it is always two distinct steps in code.

You create constraints from a visual format string using the NSLayoutConstraint method:

+ (NSArray *)constraintsWithVisualFormat:(NSString *)format

options:(NSLayoutFormatOptions)opts

metrics:(NSDictionary *)metrics

views:(NSDictionary *)views

This method returns an array of NSLayoutConstraint objects because a visual format string typically creates more than one constraint.

The first argument is the visual format string. For now, you can ignore the next two arguments, but the fourth is critical.

The fourth argument is an NSDictionary that maps the names in the visual format string to view objects in the view hierarchy. The two visual format strings that you will use to constrain the image view refer to view objects by the names of the variables that point to them.

@"H:|-[imageView]-|"

@"V:[dateLabel]-[imageView]-[toolbar]"

However, a visual format string is just a string, so putting the name of a variable inside it means nothing unless you explicitly make the association.

In BNRDetailViewController.m, create a dictionary of names for the views at the end of viewDidLoad.

[self.view addSubview:iv];

self.imageView = iv;

NSDictionary *nameMap = @{@"imageView" : self.imageView,

@"dateLabel" : self.dateLabel,

@"toolbar" : self.toolbar};

}

You are using the names of your variables as keys, but you can use any key to name a view. The only exception is the | character, which is a reserved name for the superview (container) of the views being referenced in the string.

Next, in BNRDetailViewController.m, create the horizontal and vertical constraints for the image view:

- (void)viewDidLoad

{

[super viewDidLoad];

...

NSDictionary *nameMap = @{@"imageView" : self.imageView,

@"dateLabel" : self.dateLabel,

@"toolbar" : self.toolbar};

// imageView is 0 pts from superview at left and right edges

NSArray *horizontalConstraints =

[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-0-[imageView]-0-|"

options:0

metrics:nil

views:nameMap];

// imageView is 8 pts from dateLabel at its top edge...

// ... and 8 pts from toolbar at its bottom edge

NSArray *verticalConstraints =

[NSLayoutConstraint constraintsWithVisualFormat:

@"V:[dateLabel]-[imageView]-[toolbar]"

options:0

metrics:nil

views:nameMap];

}

Adding Constraints

You now have two arrays of NSLayoutConstraint objects. However, these constraints will have no effect on the layout until you explicitly add them using the UIView method

- (void)addConstraints:(NSArray *)constraints

Which view should receive the addConstraints: message? Usually, the closest common ancestor of the views that are affected by the constraint. Here is a list of rules you can follow to determine which view you should add constraints to:

· If a constraint affects two views that have the same superview (such as the constraint labeled “A” in Figure 16.1), then the constraint should be added to their superview.

· If a constraint affects just one view (the constraint labeled “B”), then the constraint should be added to the view being affected.

· If a constraint affects two views that do not have the same superview but do share a common ancestor much higher up on the view hierarchy (the constraint labeled “C”), then the first common ancestor gets the constraint.

· If a constraint affects a view and its superview (the constraint labeled “D”), then this constraint will be added to the superview.

Figure 16.1 Constraint hierarchy

Constraint hierarchy

For the image view’s horizontal constraints, this determination is easy. These constraints affect only the imageView and its superview, so you add them to the superview – the view of the BNRDetailViewController.

For the vertical constraints, the imageView, dateLabel, and toolbar are the affected views. They all share the same superview (the view of the BNRDetailViewController), so you also add these constraints to the superview.

In BNRDetailViewController.m, add both sets of constraints to the BNRDetailViewController’s view at the end of viewDidLoad.

...

NSArray *verticalConstraints =

[NSLayoutConstraint constraintsWithVisualFormat:

@"V:[dateLabel]-[imageView]-[toolbar]"

options:0

metrics:nil

views:nameMap];

[self.view addConstraints:horizontalConstraints];

[self.view addConstraints:verticalConstraints];

}

Build and run the application. Create an item and select an image. Your detail interface may look all right or it may not. It depends on the size of the image that you selected. If the image you selected is small, you may be looking at something like this:

Figure 16.2 Small-size image gives unexpected results

Small-size image gives unexpected results

To understand what is going on, let’s look at a view’s intrinsic content size and how it interacts with Auto Layout.

(Note: If your valueField is missing, this is due to a current bug in Auto Layout. Baseline constraints – like the one between valueField and the Value label – are not being respected. Remove that baseline constraint, and replace it with a CenterY constraint. If you use the Control-click and drag approach to create this constraint, it will set the constant to the current difference between centers, in effect maintaining the baseline alignment.)

Intrinsic Content Size

Intrinsic content size is information that a view has about how big it should be based on what it displays. For example, a label’s intrinsic content size is based on how much text it is displaying. In your case, the image view’s intrinsic content size is the size of the image that you selected.

Auto Layout takes this information into consideration by creating intrinsic content size constraints for each view. Unlike other constraints, these constraints have two priorities: a content hugging priority and a content compression resistance priority.

Content hugging priority

tells Auto Layout how important it is that the view’s size stay close to, or “hug”, its intrinsic content. A value of 1000 means that the view should never be allowed to grow larger than its intrinsic content size. If the value is less than 1000, then Auto Layout may increase the view’s size when necessary.

Content compression resistance priority

tells Auto Layout how important it is that the view avoid shrinking, or “resist compressing”, its intrinsic content. A value of 1000 means that the view should never be allowed to be smaller than its intrinsic content size. If the value is less than 1000, then Auto Layout may shrink the view when necessary.

In addition, both priorities have separate horizontal and vertical values so that you can set different priorities for a view’s height and width. This makes a total of four intrinsic content size priority values per view.

You can see and edit these values in Interface Builder. Reopen BNRDetailViewController.xib. Shift-click to select all three text fields in the canvas. Head to the inspector and select the Intrinsic Content Size tab to reveal the size inspector. Find the Content Hugging Priority and Content Compression Resistance Priority sections.

Figure 16.3 Content priorities

Content priorities

First, notice that these values are not 1000 and thus will never conflict with the constraints that you have added so far. This is why the layout will appear incorrectly with smaller-sized images. The value text field’s content hugging vertical property is 250, which is lower than that of the image view (which is 251), so when faced with a small image, Auto Layout chooses to make the text field taller than its intrinsic content size.

It would be better if the image view had a smaller vertical content hugging and commpression resistance priority than the other subviews. Open BNRDetailViewController.m and update viewDidLoad to lower these priorities.

- (void)viewDidLoad

{

[super viewDidLoad];

UIImageView *iv = [[UIImageView alloc] initWithImage:nil];

// The contentMode of the image view in the XIB was Aspect Fit:

iv.contentMode = UIViewContentModeScaleAspectFit;

// Do not produce a translated constraint for this view

iv.translatesAutoresizingMaskIntoConstraints = NO;

// The image view was a subview of the view

[self.view addSubview:iv];

// The image view was pointed to by the imageView property

self.imageView = iv;

// Set the vertical priorities to be less than

// those of the other subviews

[self.imageView setContentHuggingPriority:200

forAxis:UILayoutConstraintAxisVertical];

[self.imageView setContentCompressionResistancePriority:700

forAxis:UILayoutConstraintAxisVertical];

...

}

Build and run again. Now, when dealing with smaller-sized images, Auto Layout will change the image size and leave the height of the text fields alone.

The Other Way

There are times when a constraint cannot be created with a visual format string. For instance, you cannot use VFL to create a constraint based on a ratio, like if you wanted the date label to be twice as tall as the name label or if you wanted the image view to always be 1.5 times as wide as it is tall.

In these cases, you can create an instance of NSLayoutConstraint using the method

+ (id)constraintWithItem:(id)view1

attribute:(NSLayoutAttribute)attr1

relatedBy:(NSLayoutRelation)relation

toItem:(id)view2

attribute:(NSLayoutAttribute)attr2

multiplier:(CGFloat)multiplier

constant:(CGFloat)c

This method creates a single constraint using two layout attributes of two view objects. The multiplier is the key to creating a constraint based on a ratio. The constant is a fixed number of points, like you have used in your spacing constraints.

The layout attributes are defined as constants in the NSLayoutConstraint class:

· NSLayoutAttributeLeft

· NSLayoutAttributeRight

· NSLayoutAttributeTop

· NSLayoutAttributeBottom

· NSLayoutAttributeWidth

· NSLayoutAttributeHeight

· NSLayoutAttributeBaseline

· NSLayoutAttributeCenterX

· NSLayoutAttributeCenterY

· NSLayoutAttributeLeading

· NSLayoutAttributeTrailing

Let’s consider a hypothetical constraint. Say you wanted the image view to be 1.5 times as wide as it is tall. You cannot do this with a visual format string, so you would create it individually instead with the following code. (Do not type this hypothetical constraint in your code! It will conflict with others you already have.)

NSLayoutConstraint *aspectConstraint =

[NSLayoutConstraint constraintWithItem:self.imageView

attribute:NSLayoutAttributeWidth

relatedBy:NSLayoutRelationEqual

toItem:self.imageView

attribute:NSLayoutAttributeHeight

multiplier:1.5

constant:0.0];

To understand how this method works, think of this constraint as the equation shown in Figure 16.4.

Figure 16.4 NSLayoutConstraint equation

NSLayoutConstraint equation

You relate a layout attribute of one view to the layout attribute of another view using a multiplier and a constant to define a single constraint.

To add a single constraint to a view, you use the method

- (void)addConstraint:(NSLayoutConstraint *)constraint

The same logic applies to decide which view should receive this message. When using this method, the determination is even easier to make because the affected view objects are the first and fourth arguments. In this case, the only affected object is the image view, so you would addaspectConstraint to that view:

[self.imageView addConstraint:aspectConstraint];

For the More Curious: NSAutoresizingMaskLayoutConstraint

Before Auto Layout, iOS applications used another system for managing layout: autoresizing masks. Each view had an autoresizing mask that constrained the relationship between a view and its superview, but this mask could not affect relationships between sibling views.

By default, views create and add constraints based on their autoresizing mask. However, these translated constraints often conflict with your explicit constraints in your layout, which results an unsatisfiable constraints problem.

To see this happen, comment out the line in viewDidLoad that turns off the translation of autoresizing masks.

// The contentMode of the image view in the XIB was Aspect Fit:

iv.contentMode = UIViewContentModeScaleAspectFit;

// Turn off old-school layout handling

iv.translatesAutoresizingMaskIntoConstraints = NO;

// The image view was a subview of the view

[self.view addSubview:iv];

Now the image view has a resizing mask that will be translated into a constraint. Build and run the application and navigate to the detail interface. You will not like what you see. The console will report the problem and its solution.

Unable to simultaneously satisfy constraints.

Probably at least one of the constraints in the following list is one you don't

want. Try this: (1) look at each constraint and try to figure out which you don't

expect; (2) find the code that added the unwanted constraint or constraints and

fix it. (Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't

understand, refer to the documentation for the UIView property

translatesAutoresizingMaskIntoConstraints)

(

"<NSLayoutConstraint:0x914a2e0 H:[UILabel:0x914a1e0(42)]>",

"<NSLayoutConstraint:0x9153ee0

H:|-(20)-[UILabel:0x9149f00] (Names: '|':UIControl:0x91496e0 )>",

"<NSLayoutConstraint:0x9153fa0

UILabel:0x9149970.leading == UILabel:0x9149f00.leading>",

"<NSLayoutConstraint:0x91540c0

UILabel:0x914a1e0.leading == UILabel:0x9149970.leading>",

"<NSLayoutConstraint:0x9154420

H:[UITextField:0x914fe20]-(20)-| (Names: '|':UIControl:0x91496e0 )>",

"<NSLayoutConstraint:0x9154450

H:[UILabel:0x914a1e0]-(12)-[UITextField:0x914fe20]>",

"<NSLayoutConstraint:0x912f5a0

H:|-(NSSpace(20))-[UIImageView:0x91524d0] (Names: '|':UIControl:0x91496e0 )>",

"<NSLayoutConstraint:0x91452a0

H:[UIImageView:0x91524d0]-(NSSpace(20))-| (Names: '|':UIControl:0x91496e0 )>",

"<NSAutoresizingMaskLayoutConstraint:0x905f130

h=--& v=--& UIImageView:0x91524d0.midX ==>"

)

Will attempt to recover by breaking constraint

<NSLayoutConstraint:0x914a2e0 H:[UILabel:0x914a1e0(42)]>

Let’s go over this output. Auto Layout is reporting that it is “Unable to simultaneously satisfy constraints.” This happens when a view hierarchy has constraints that conflict.

Then, the console spits out some handy tips and a list of all constraints that are involved. Each constraint’s description is shown in the console. Let’s look at the format of one of these constraints more closely.

<NSLayoutConstraint:0x9153fa0 UILabel:0x9149970.leading == UILabel:0x9149f00.leading>

This description indicates that the constraint located at memory address 0x9153fa0 is setting the leading edge of the UILabel (at 0x9149970) equal to the leading edge of the UILabel (at 0x9149f00).

Four of these constraints are instances of NSLayoutConstraint. The fifth, however, is an instance of NSAutoresizingMaskLayoutConstraint. This constraint is the product of the translation of the image view’s autoresizing mask.

Finally, it tells you how it is going to solve the problem by listing the conflicting constraint that it will ignore. Unfortunately, it chooses poorly and ignores one of your explicit instances of NSLayoutConstraint instead of the NSAutoresizingMaskLayoutConstraint. This is why your interface looks like it does.

The note before the constraints are listed is very helpful: the NSAutoresizingMaskLayoutConstraint needs to be removed. Better yet, you can prevent this constraint from being added in the first place by explicitly disabling translation in viewDidLoad:

// The contentMode of the image view in the XIB was Aspect Fit:

iv.contentMode = UIViewContentModeScaleAspectFit;

// Do not produce a translated constraint for this view

iv.translatesAutoresizingMaskIntoConstraints = NO;

// The image view was a subview of the view

[self.view addSubview:iv];