Visual Formats - iOS Auto Layout Demystified, Second Edition (2014)

iOS Auto Layout Demystified, Second Edition (2014)

Chapter 4. Visual Formats

Auto Layout builds constraints in three ways. So far, you’ve read about two of them. First, you can lay out your constraints in Interface Builder (IB) and customize them to your needs. Second, you can build single constraints in code. The NSLayoutConstraint class offers theconstraintWithItem: attribute:relatedBy:toItem:attribute:multiplier:constant: method, which enables you to create constraints one at a time, relating one item’s attribute to another. In this chapter, you’ll read about the third way: using a visual formatting language to express how items are laid out along vertical and horizontal axes.

This chapter explores what these visual constraints look like, how you build them, and how you use them in your projects. You’ll read about how metrics dictionaries and constraint options extend visual formats for more flexibility. And you’ll see numerous examples that demonstrate these formats and explore the results they create.

There’s one thing to keep in mind throughout: All constraints are members of the NSLayoutConstraint class, regardless of how you build them. Every constraint stores a “y relation mx + b” rule within an Objective-C object and expresses that rule through the Auto Layout engine. Visual formats are another tool that takes you to that same place.

Introducing Visual Format Constraints

As with individual constraints, you build visual format constraints by calling an NSLayoutConstraint class method. Although visual formats can relate any number of views, they translate down to instances that relate just one or two views at a time. You supply a text-based specification and any options, and the class creates a group of constraints from that description.

The visual format consists of a text string that describes the view layout. You list items in sequence as they appear in the interface. Text sequences specify spacing, inequalities, and priorities. The result is a short visual picture of the layout in text. In a way, it’s a bit like ASCII art for Objective-C nerds.

The following code snippet demonstrates constraint creation using visual formats. I boldfaced the two key items in this request. They are the visual format itself and an option that says how to align the layout:

[self.view addConstraints: [NSLayoutConstraint
constraintsWithVisualFormat:@"V:[view1]-8-[view2]"
options:NSLayoutFormatAlignAllLeading
metrics:nil
views:NSDictionaryOfVariableBindings(view1, view2)]];

This call creates a pair of constraints that say “Create a left-aligned vertical column of view1 followed by view2, leaving an 8-point spacer between the two views.” The boldfaced items are the visual format and options parameters. Here are a few things to note about how this constraints formatting example is created:

Image The axis (or orientation, if you’re using OS X) is specified first as a prefix, either H: or V:. When you omit the axis, the constraint defaults to horizontal layout. I encourage you to always use a prefix. Mandatory prefixes provide a consistent indication of design intent, ensuring that any missing prefix is guaranteed to be a mistake.

Image Variable names for each view appear in square brackets (for example, [view1]).

Image The order of the view names in the string matches the requested order of the views in the layout. The order is normally from top to bottom or left to right. In Arabic and Hebrew locales, the order is right to left. You can also override the order with layout format options.

Image The fixed spacing appears between the two views as a number constant, -8-. Hyphens surround the number.

Image The options parameter specifies alignment. In this example, it sets a leading alignment, which is left-aligned for English-like languages and right-aligned for languages like Arabic and Hebrew. Leading refers to the first horizontal edge encountered for the standard writing direction of the prevailing locale. Trailing refers to the final horizontal edge.

Image A metrics dictionary parameter is not included in this example. When used, this parameter supplies constant numbers for value substitutions in constraints. For example, if you want to vary the spacing between these two views, you could replace 8 with a metric name like myOffsetand assign that metric’s value via a dictionary.

Image The views: parameter does not, despite its name, pass an array of views. It passes a dictionary of variable bindings. This dictionary associates variable name strings (for example, "view1") with the objects they represent (the view instance whose variable name is view1). This indirection allows you to use developer-meaningful symbols like "nameLabel" and "requestButton" in your format strings.

This example creates two constraints. Visual format strings always produce an array of results. Some format strings are quite complex, and others are simple. It’s not always easy to guess how many constraints will be generated from each string. You install the entire collection of constraints to satisfy the format string that you processed. Here are the two constraints for this example:

[NSLayoutConstraint
constraintWithItem:view2
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:view1
attribute:NSLayoutAttributeBottom
multiplier:1.0
constant:8.0];
[NSLayoutConstraint
constraintWithItem:view1
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:view2
attribute:NSLayoutAttributeLeading
multiplier:1.0
constant:0.0];

The first constraint aligns view2’s top to view1’s bottom and adds 8 points of spacing. This constraint derives from the visual format string. The second constraint is produced from the options argument. It aligns both views’ leading edges, which is the left edge in English’s left-to-right layout system.

So, why build with visual formats if the results are identical to manually built constraints?

Image First, they are more concise. A single visual format can express layout conditions that would take several constraints to describe.

Image Second, they are more easily inspected. The visual format tells a little layout story, allowing you to focus your attention on more concentrated ideas.

Image Third, they can be easily tweaked. If you want to update the alignment or adjust the spacing, you have to modify just one call.

Apple recommends using visual format constraints over standard layout constraints and prefers IB solutions to code-based ones. I recommend that you use the layout solutions that best match your individual development comfort.

Options

Table 4-1 lists the options you can supply to the visual format method. These options include alignment masks (left column) and formatting directions (right column). You can supply only one format direction. Prior to iOS 7, Apple also required that you choose only one alignment mask at a time. That requirement is now gone. To combine options, you use a bitwise OR.

Image

Table 4-1 Layout Options

For most layout work, you need only the values from the left column of Table 4-1. These choices set the alignment used to augment the visual format you specify. In a rare case in which you need to flip formatting directions, you use the options in the right column of Table 4-1. The leading-to-trailing direction is the default. You do not have to set it explicitly.

Alignment

You should always apply alignment masks perpendicular to your format. When you create a horizontal row, you specify a vertical alignment (and vice versa). For example, imagine laying out a row of objects from left to right—for example, H:[view1]-[view2]-[view3]-[view4]. You can align their tops, their middles, or their bottoms simply by tweaking them up or down a bit.

However, you can’t align their lefts or rights because doing so would force them out of the layout order you specified. All the views would have to scoot left or scoot right, essentially contradicting the visual format.

Breaking this rule raises an exception, as you see in the following log text. In this case, I attempted to apply a horizontal alignment (leading edges) to a horizontal constraint (H:[view1]-8-[view2]):

2013-01-22 12:35:23.885 HelloWorld[25429:c07] *** Terminating app due to uncaught
exception 'NSInvalidArgumentException', reason: 'Unable to parse constraint format:
Options mask required views to be aligned on a horizontal edge, which is not allowed
for layout that is also horizontal.

Skipping Options

You skip options by supplying 0 to the options parameter. NSLayoutConstraint builds constraints from the visual format you supply but doesn’t add any alignment constraints:

[self.view addConstraints: [NSLayoutConstraint
constraintsWithVisualFormat:@"H:[view1]-8-[view2]"
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(view1, view2)]];

Variable Bindings

When you’re working with visual constraints, the layout system associates name strings like “view1” and “view2” with the objects they represent. Variable bindings build these associations. You create them by calling NSDictionaryOfVariableBindings(), a macro defined inNSLayoutConstraint.h. You pass the macro an arbitrary number of local view variables, as in this example:

NSDictionaryOfVariableBindings(view1, view2, view3, view4)

As you see, this macro doesn’t require a nil semaphore to terminate your list. It builds a dictionary from the passed variables, using the variable names as keys and the objects they point to as values. For example, this call:

NSDictionaryOfVariableBindings(leftLabel, rightLabel)

builds this dictionary:

@{@"leftLabel":leftLabel, @"rightLabel":rightLabel}.

If you’d rather not use the variable bindings macro, you can easily create a dictionary by hand and pass it to the visual format constraints builder. Visual constraints don’t work well with view arrays, which is one reason you should create a custom bindings dictionary.

The Problem with Indirection

Consider the following code, which attempts to create a visual format around a view array:

NSDictionary *bindings =
NSDictionaryOfVariableBindings(views[0], views[1]);
constraints = [NSLayoutConstraint constraintsWithVisualFormat:
@"H:|[views[0]]-[views[1]]|" options:0 metrics:nil views:bindings];

Here is the bindings dictionary built by this call:

2013-01-24 09:40:17.403 HelloWorld[46260:c07]
{
"views[0]" = "<TestView: 0x8a6af50; frame = (0 0; 0 0);
layer = <CALayer: 0x8a6b010>>";
"views[1]" = "<TestView: 0x8a6e850; frame = (0 0; 0 0);
layer = <CALayer: 0x8a6e8c0>>";
}

Although this compiles without error, it raises a runtime exception. The format string parser cannot handle the indexed view references. You are, essentially, embedding Objective-C calls into the format, which is a step too far for the NSLayoutConstraint class:

2013-01-24 09:12:02.374 HelloWorld[45646:c07] *** Terminating app due to uncaught
exception 'NSInvalidArgumentException', reason: 'Unable to parse constraint format:
views is not a key in the views dictionary.
H:|[views[0]]-[views[1]]|

Indirection Workaround

As the example you just saw demonstrates, visual formats work poorly with any items that are not explicitly declared in the local context. Visual formats depend on simple name-to-instance translations. The way you refer to non-local instances in code (for example, [self.view viewWithTag:99] or views[0]) doesn’t parse correctly within the format string.

To work around this, you can build both the dictionary and your format string from code. The following code snippet demonstrates one approach:

// Initialize the format string and bindings dictionary
NSMutableString *formatString = [NSMutableString string];
NSMutableDictionary *bindings = [NSMutableDictionary dictionary];

// View counter
int i = 1;

// Build a format string that lays out the views in a row
// e.g. @"H:|-[view1]-[view2]-[view3]..."
[formatString appendString:@"H:|-"];
for (UIView *view in views)
{
// Create a view name
NSString *viewName =
[NSString stringWithFormat:@"view%0d", i++];

// Add the new view to the layout string
[formatString appendFormat:@"[%@]%@",
viewName, (i <= views.count) ? @"-" : @""];

// Store the view and its name in the bindings dictionary
bindings[viewName] = view;
}

Assigning each item a name and an entry in the bindings dictionary allows you to work around the impossibility of indexing items from within the format string. Once populated, this dictionary can be passed as the variable bindings parameter for the formatString built by this method:

// Build constraints with centerY alignment
constraints = [NSLayoutConstraint
constraintsWithVisualFormat:formatString
options:NSLayoutFormatAlignAllCenterY
metrics:nil views:bindings];

// Install the constraints
[self.view addConstraints:constraints];

Metrics

When you don’t know a constant’s value a priori, a metrics dictionary supplies that value to your visual format string. For example, consider this:

@"V:[view1]-spacing-[view2]"

Here, the word spacing represents some spacing value, which has not yet been determined. You pass that value by building a dictionary that equates the word string with its value. For example, this dictionary associates spacing with the number 10:

NSDictionary *metrics = @{@"spacing":@10};

You supply this dictionary to the metrics parameter of the visual format creation method:

[NSLayoutConstraint
constraintsWithVisualFormat:@"V:[view1]-spacing-[view2]"
options:NSLayoutFormatAlignAllCenterX
metrics:metrics
views:bindings];

NSLayoutConstraint uses the metrics dictionary to substitute the value 10 for the spacing string. Unlike the bindings dictionary, which equates views to names, the metrics dictionary supplies numbers.

Real-World Metrics

Metrics are most useful when you build constraints programmatically. For example, consider the following method. It constrains a view’s width to a size you supply. When writing this method, you cannot know what view will be supplied or what width it needs to be constrained to:

- (void) constrainView: (VIEW_CLASS *) view toWidth: (CGFloat) width
{
NSString *formatString = @"H:[view(==width)]";
NSDictionary *bindings = NSDictionaryOfVariableBindings(view);
NSDictionary *metrics = @{@"width":@(width)};

NSArray *constraints = [NSLayoutConstraint
constraintsWithVisualFormat:formatString
options:0 metrics:metrics views:bindings];
[view addConstraints:constraints];
}

The metrics dictionary used here associates the "width" string in the visual format with the width parameter passed to the method. This allows you to construct readable, maintainable format strings using parameter indirection.


Note

VIEW_CLASS is used throughout this book to refer to UIView on iOS and NSView in OS X.


Format String Structure

The format strings used to create constraints follow a grammar, which is specified as follows:

(<orientation>:)? (<superview><connection>)? <view>(<connection><view>)*
(<connection><superview>)?

The question marks refer to an optional item, and the asterisk refers to an item that may appear zero or more times.

Although this definition is daunting to look at, these strings are actually quite easy to construct. The sections that follow offer an introduction to these format string elements and provide examples of their use.

Orientation

Visual formats start with an optional orientation, either H: for horizontal or V: for vertical alignment. This alignment specifies whether the constraint applies from the leading edge toward the trailing edge or from the top downward.

Consider this constraint format: "[view1][view2]". When prefixed with V:, it says to place View 2 just below View 1. With H:, it says to place View 2 to the right of View 1. Figure 4-1 shows these two constraint requests displayed in a simple iOS test bed application.

Image

Figure 4-1 These two images were built with "V:[view1][view2]" (left) and "H:[view1] [view2]" (right) visual format strings.

You can produce a horizontal layout by skipping the orientation prefix, although I don’t recommend you do so. Mandatory prefixes enhance your code checks, helping you find mistakes at a glance. The horizontal axis for both iOS and OS X is the default.

Retrieving Constraints by Axis

Because Auto Layout design is axis specific, you might want to separate a view’s constraints into horizontal and vertical members. During debugging, you may use the constraints AffectingLayoutForAxis: view method (this is constraintsAffectingLayoutFor Orientation: on OS X) to retrieve all constraints that affect either the horizontal or vertical layout. This code isn’t intended for deployment use, and Apple states in its documentation that it is not App Store-safe. The horizontal axis is enumerated with a value of 0. The vertical axis is enumerated with 1.

View Names

As the two examples you just saw demonstrate, view names are encased in square brackets. For example, you might work with "[thisview]" and "[thatview]". When working with a variable bindings dictionary, the view name refers to the local variable name of your view. So if you’ve declared this:

UIButton *myButton;

your format string can refer to "[myButton]".

As you saw earlier in this chapter, you associate view names with view instances through a dictionary of variable bindings. You pass the dictionary to the constraint creation method to map names to objects.

Superviews

A special character, the vertical pipe (|) always refers to the superview. You see it only at the beginning or end of format strings. At the beginning, it appears just after the horizontal or vertical specifier ("V:|..." or "H:|..."). At the end, it appears just before the terminating quote character ("...|").

You do not need to name the superview in your bindings dictionary. Auto Layout understands that | refers to the view’s superview.

Typical cases for using the superview character include the following:

Image Stretching a view to fit its superview—for example, "H:|[view]|"

Image Offsetting a view from its superview’s edge—for example, "V:[view]-8-|"

Image Creating a superview-aligned row or column of views—for example, "V:|-[view1]-[view2]-[view3]-[view4]"

Connections

Connections specify inter-view spacing. Listed between each view (including superview references), they mark out the distance to add. The following discussion surveys the connection types to use in your apps.

Empty Connections

An empty connection looks like this: H:[view1][view2] or V:|[view3]. Nothing is specified between the square brackets of View 1 and View 2, or between the vertical pipe and the square bracket of View 3. These examples respectively lay out View 2 directly to the right of View 1 (see the top image in Figure 4-2) and View 3 at the very top of its superview (see the bottom image in Figure 4-2).

Image

Figure 4-2 Top: "H:[view1][view2]". Bottom: "V:|[view3]". Omitting connections forces views to abut along the constraint axis.

You use the empty connection to place views that should naturally abut each other. For example, you may need segmented view art to work together to form a single natural presentation onscreen and operate as a single entity. The empty connection allows the views to lay one directly after the other. The empty connection specified in the visual format relating view1 and view2 creates an NSLayoutConstraint instance that relates view1’s NSLayoutAttributeTrailing edge to view2’s NSLayoutAttributeLeading edge using an equality relation.

Standard Spacers

A hyphen (-) represents a standard fixed space. The constraint "H:|-[view1]-[view2]" leaves a small gap between View 1 and View 2 and between View 1 and its superview (see Figure 4-3). Although officially undocumented, this standard is generally 8 points for view-to-view layout and 20 points for view-to-superview layout.

Image

Figure 4-3 "H:|-[view1]-[view2]" adds standard spacer connections between the views.

Apple engineers have stated that visual formats use “Aqua spacing” for layout. These 8- and 20-point values derive from Apple’s Aqua user interface standards, the primary visual theme used in OS X design.

Standard gaps ensure that related but distinct views are shown with sufficient visual space. For example, you might use spacers to offset labels from the controls (switches, buttons, text fields, and so on) they describe.

Numeric Spacers

A numeric constant placed between hyphens sets an exact gap size. The constraint "H:[view1]-30-[view2]" adds a 30-point gap between the two views, as shown in Figure 4-4. This is visibly wider than the small default gap produced by the single hyphen, shown in Figure 4-3.

Image

Figure 4-4 "H:[view1]-30-[view2]" uses a fixed-size gap of 30 points, producing a noticeably larger distance between the two views than the standard spacer.

Referencing the Superview

The format "H:|[view1]-[view2]|" specifies a horizontal layout that starts with the superview. Notice the vertical pipe to the right of the axis specifier. The superview is immediately followed by the first view, then a spacer, the second view, and then, with another vertical pipe, the superview.

The constraint left-aligns View 1 and right-aligns View 2 flush with the superview. To accomplish this, something has to give. Either the left view or the right view must resize. When I ran the test app, it happened to be View 1 that adjusted, as you can see in Figure 4-5. It could just as easily have been View 2.

Image

Figure 4-5 "H:|[view1]-[view2]|" tells both views to hug the edges of their superview. With a fixed-size gap between them, at least one of the views must resize to satisfy the constraint.

Spacing from the Superview

Often, you don’t want to bang right up against the superview edges. The "H:|-[view1]-[view2]-|" format adds an inset between the edges of the superview and the start of View 1 and end of View 2 (see Figure 4-6). The standard superview inset (20 points) is wider than the view-to-view gap (8 points).

Image

Figure 4-6 "H:|-[view1]-[view2]-|" introduces edge insets between the views and their superviews.

Insets give your views visual breathing room, allowing them to move away from the edges of the screen or a parent window. If you need to inset with a nonstandard distance, you can specify a number between hyphens (for example, "H:[view2]-50-|").

Flexible Spaces

If your goal is to add a flexible space between views, there’s a way to do that, too. You add a relation rule between the two views (for example, "H:|-[view1]-(>=0)-[view2]-|") to allow the two views to retain their sizes and separate while maintaining gaps at their edges with the superview, as shown in Figure 4-7.

Image

Figure 4-7 "H:|-[view1]-(>=0)-[view2]-|" uses a flexible space between the two views, allowing them to separate while maintaining their sizes.

This rule, which you can read as “at least 0 points distance,” provides a more flexible way to let the views spread out. By using a small number here, you don’t inadvertently interfere with a view’s other geometry rules.

While you can say “at least 50 points” (>=50) or “no more than 30 points” (<=30), you cannot relate distance to the standard (Aqua) space. For example, saying >=- or <=- is illegal. You just use equivalent numeric values. Remember that standard view-to-view spacing is 8 points, and view-to-superview spacing is 20 points.

Parentheses

There’s an important syntactic difference between the format used in Figure 4-7 ("H:|-[view1]-(>=0)-[view2]-|") and a fixed spacing format (for example, "H:|-[view1]-30-[view2]-|")—namely that the relation (>=0) has been placed within parentheses for clarity. Parentheses distinguish spacing that’s not a simple positive number or metric name.

You have a lot of flexibility with visual constraints in how you express rules. The following rules all have two views that abut one another:

Image [view1][view2]

Image [view1]-0-[view2]

Image [view1]-(0)-[view2]

Image [view1]-(==0)-[view2]

Image [view1]-(>=0,<=0)-[view2]

Image [view1]-(==0@1000)-[view2]

Image [view1]-(>=0,<=0,==0,<=30)-[view2]

When you add multiple relations within parentheses, you separate items with commas. The @ sign specifies a priority, as you’ll see in the “Priorities” section.

Negative Numbers

You must use parentheses for any spacer that involves a negative number. These constraints are both illegal:

Image V:[view1]--5-[view2]

Image V:[view1]- -5-[view2]

The errors raised by these constraints are as follows. The first item complains about the repeated - sign:

2013-01-31 18:52:27.735 HelloWorld[88684:c07] *** Terminating app due to uncaught
exception 'NSInvalidArgumentException', reason: 'Unable to parse constraint format:
Cannot tell if this - is a minus sign or an accidental extra bar in the connection.
Use parentheses around negative numbers.
V:[v1]--5-[v2]

The second catches on the space before the minus:

2013-01-31 18:53:18.756 HelloWorld[88713:c07] *** Terminating app due to uncaught
exception 'NSInvalidArgumentException', reason: 'Unable to parse constraint format:
Expected a number or key from the metrics dictionary, but encountered something else
V:[v1]- -5-[v2]

There is a legal solution. The following constraint says, “Set the top edge of view 2 to 5 points above the bottom edge of View 1”:

V:[view1]-(-5)-[view2]

Negative constants are allowed in visual constraints.

Priorities

You prioritize layout requests by adding an optional value to the format string. Append any connection or sizing rule with an @ sign followed by the numeric priority you want to assign.

For example, in this visual format:

"H:|-5@20-[view1]-[view2]-|"

the first spacing request between the superview and View 1 is prioritized at 20, a very low value. Normally, spacing rules are required unless you specify otherwise.

For clarity, you can surround the spacing request with parentheses, although they are not strictly necessary in this case:

"H:|-(5@20)-[view1]-[view2]-|"

Because you’ve lowered the priority to 20, the way the two views are sized may affect layout. For example, if the views are small due to a high priority, they may not end up reaching the left side of the screen at all. Sized large, they might shoot off too far to the left. The rule that links them to the superview’s left side may no longer be important enough to pin down the leftmost edge.

Multiple Views

Format constraints are not limited to just one or two views. You can easily stick in a third, fourth, or more. Consider this constraint string:

"H:|-[view1]-[view2]-(>=8)-[view3]-|"

It adds a third view, separated from the other two views by a flexible space that is at least a standard distance wide. Figure 4-8 shows what this might look like. This approach enables you to specify entire rows or columns at a time, using a single constraint request.

Image

Figure 4-8 "H:|-[view1]-[view2]-(>=8)-[view3]-|" demonstrates a rule that references three views.

View Sizes

The visual constraint formatting language optionally specifies view sizing within the square brackets that otherwise delimit the view name. You add the sizing specifications in parentheses after the name, like this:

Image You might specify a view with a fixed 120-point width: @"H:[view1(120)]". If you prefer to state the relation explicitly, you can add that as well: @"H:[view1(==120)]".

Image You might specify that the width of a view is at least 50 points, using the following format: @"H:[view1(>=50)]". A similar approach lets a view’s size range between 50 and 70 points. As with spacing, separate your rules with commas for compound items: "H:[view1(>=50,<=70)]".

Image You can refer to other views in sizing requests. For example, this format matches View 1’s width to View 2’s width: "H:[view1(view2)]". If you add size matching to the "H:|-[view1]-[view2]-|" format originally shown in Figure 4-6, you ensure that both subviews are the same size, fixing the lopsided random layout (that is, "H:|-[view1(view2)]-[view2]-|"). Figure 4-9 shows the updated result.

Image

Figure 4-9 "H:|-[view1(view2)]-[view2]-|". Adding size matching ensures that both views have equal widths.

Incidentally, because constraints are nondirectional and can be self-referential, you produce an equivalent layout with this format: "H:|-[view1(view2)]-[view2(view1)]-|", where View 1 matches View 2 and View 2 matches View 1. Circular definitions don’t produce performance hits.

Image Not all views need to participate in size matching. The next request creates matching flanking views around a primary view while stretching all three views across the superview: @"H:|-[view1(<=80)]-[view2]-[view3(view1)]-|". The format limits View 1’s size to 80 points and matches it to View 3, ensuring that View 2 stretches to occupy the remaining space. Figure 4-10 shows the resulting layout.

Image

Figure 4-10 With "H:|-[view1(<=80)]-[view2]-[view3(view1)]-|", the two outer views are matched in size and limited to a maximum width, allowing View 2, which has no real intrinsic size, to expand to occupy the remaining room.

Image View sizes can also express priorities. In the format string "H:|-[view1(==250@700)]-[view2(==250@701)]-|", both View 1 and View 2 request to be 250 points wide. View 2 wins (see Figure 4-11) because its request has a higher priority.

Image

Figure 4-11 With "H:|-[view1(==250@700)]-[view2(==250@701)]-|", View 2’s request to be 250 points wide wins as it has a higher priority.

Image Although you can easily produce constraints in code that express relative size using multipliers, you cannot do so in visual constraints. This is an illegal constraint: "H:|-[view1(==2*view2)]-[view2]-|". If you want to say “View 1 is twice the width of View 2,” you need to do so in code.

Format String Components

Table 4-2 summarizes the components used to create layout constraints through visual formats. You can combine multiple conditions with commas within parentheses—for example, (>=0, <=250).

Image

Image

Table 4-2 Visual Format Strings


Note

iOS 7 and Xcode 5 introduce two properties for laying out your formats with respect to a container’s bars. The topLayoutGuide and bottomLayoutGuide conform to the UILayoutSupport protocol, establishing offsets for view controllers whose views may extend behind parent bars. At the time of writing this book, you must assign those properties to local variables to refer to them in your format strings.


Getting It Wrong

Xcode provides no compiler check that ensures the validity of your format strings. A poorly formatted string throws an exception at runtime. For example, you may have skipped a colon, as shown here:

2013-01-23 11:40:17.169 HelloWorld[35717:c07] *** Terminating app due to uncaught
exception 'NSInvalidArgumentException', reason: 'Unable to parse constraint format:
Expected ':' after 'H' to specify horizontal arrangement
H|[view1]-[view2]

Or you may have forgotten to close the square bracket ending a view declaration. Where possible, the log messages attempt to instruct you on how to fix your mistake, as shown here:

2013-01-23 11:41:35.897 HelloWorld[35750:c07] *** Terminating app due to uncaught
exception 'NSInvalidArgumentException', reason: 'Unable to parse constraint format:
Expected a ']' here. That is how you give the end of a view.
H:|[view1-[view2]

Common errors include stray or missing characters, views that don’t exist in the bindings dictionary, missing metrics values, and invalid priorities. The exceptions raised by these errors demonstrate why it’s absolutely critical to fully inspect and test all format strings.

NSLog and Visual Formats

Xcode adds visual format representations wherever it can into NSLayoutConstraint logs. Even if you build your constraints entirely in IB or through single constraint instances, you’ll be well served by learning this layout language. Here’s an example of a visual format–based log. I’ve bolded both the format and the binding between the superview pipe (|) character and the view it represents (stored at memory address 0x71c7f50):

2013-01-23 11:21:39.378 HelloWorld[35535:c07] <NSLayoutConstraint:0x71cf290 H:|-
(55@20)-[TestView:0x71c8390] priority:20 (Names: '|':UIView:0x71c7f50)>

Not all constraints can be expressed with visual formats, and even those that can be may not produce the output you expect. Consider the following code, which builds and logs what are basically two identical constraints:

// This provides no visual format
NSLayoutConstraint *c = [NSLayoutConstraint
constraintWithItem:view1
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeBottom
multiplier:1.0f constant:0.0f];
NSLog(@"%@", c);

// This basically identical constraint does.
c = [NSLayoutConstraint
constraintWithItem:self.view
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:view1
attribute:NSLayoutAttributeBottom
multiplier:1.0f constant:0.0f];
NSLog(@"%@", c);

The logs for these two constraints showcase the difference. The first log shows a y R mx + b relation, and the second shows a visual format:

2013-01-23 11:55:08.961 HelloWorld[36083:c07] <NSLayoutConstraint:0x958c650
TestView:0x9566b30.bottom == UIView:0x9568790.bottom>
2013-01-23 11:55:08.962 HelloWorld[36083:c07] <NSLayoutConstraint:0x7565790
V:[TestView:0x9566b30]-(0)-| (Names: '|':UIView:0x9568790 )>

Expect to see both of these logs in your development work. So, why does swapping the order of the two views make a difference? It’s an Apple implementation detail, and one that seems as inconsistent to me as it probably does to you.

Constraining to a Superview

Visual formats offer a great match for view fallback conditions, as you can express and implement those edge conditions using a series of format strings instead of code. You see this in Listing 4-1, where the format strings power these requirements.

This function succinctly constrains a view to its superview, using a priority you specify. The for loop iterates through four format strings, each of which describes a view boundary: The first two keep the view inside the parent’s leading and trailing edges, and the second two do the same for the top and bottom edges. Inequality relations ensure that the view’s edges are at least within those bounds, without specifying any further position.

The function adds its minimum sizing requests, using the same priority. You pass that minimum size as an argument to the function. For example, a 40×40 or 100×100 view can easily be seen in most interfaces.

You generally call this function with a very low priority, such as 1, establishing a fallback set of rules for the view. The function mandates that the view must appear onscreen and that it must have an easy-to-view size so that your views don’t go missing. You should add these rules early (for example, in your loadView or viewDidLoad methods).

A simple viewDidAppear: method can inventory your views:

- (void) viewDidAppear:(BOOL)animated
{
for (UIView *view in self.view.subviews)
NSLog(@"View: %@", NSStringFromCGRect(view.frame));
}

You’re ready to start building Auto Layout–powered interfaces where you don’t experience the “missing views” issue that plagues so many new Auto Layout developers.

Listing 4-1 Constraining Views to Their Superview


void constrainToSuperview(VIEW_CLASS *view,
float minimumSize, NSUInteger priority)
{
if (!view || !view.superview)
return;

for (NSString *format in @[
@"H:|->=0@priority-[view(==minimumSize@priority)]",
@"H:[view]->=0@priority-|",
@"V:|->=0@priority-[view(==minimumSize@priority)]",
@"V:[view]->=0@priority-|"])
{
NSArray *constraints = [NSLayoutConstraint
constraintsWithVisualFormat:format options:0
metrics: @{@"priority":@(priority),
@"minimumSize":@(minimumSize)}
views:@{@"view": view}];
[view.superview addConstraints:constraints];
}
}


View Stretching

Stretching views is another common view task you can easily address with visual formats. Listing 4-2 creates a function that stretches the view you supply to its superview, leaving an indentation that you specify. Figure 4-12 shows the interface built by ramping up the indentation for a series of four views.

Image

Figure 4-12 These stepped views were built from visual formats. The stepped indentations represent a 20-point value multiplied by the view number.

Like the previous example, this method builds constraints from an array of format strings, using a metrics dictionary to specify the indentation requested. Although this method builds both vertical and horizontal stretching rules, you could easily adapt this code for separate requests per axis.

Listing 4-2 Stretching Views to Their Superview


void stretchToSuperview(VIEW_CLASS *view,
CGFloat indent, NSUInteger priority)
{
for (NSString *format in @[
@"H:|-indent-[view]-indent-|",
@"V:|-indent-[view]-indent-|"
])
{
NSArray *constraints = [NSLayoutConstraint
constraintsWithVisualFormat:format options:0
metrics:@{@"indent":@(indent)} views:@{@"view": view}];
for (NSLayoutConstraint *constraint in constraints)
{
constraint.priority = priority;
[view.superview addConstraint:constraint];
}
}
}


Constraining Size

Listing 4-3 uses visual formats to constrain a view to the size that you specify. You choose the priority, which can range from a mild suggestion all the way up to required. The method adds two visual constraints and sets their exact sizes. You can easily adapt this function to support minimum and maximum sizes by simply replacing the equalities in the two strings with inequalities. If you do this, consider creating a private helper function to do the work without redundancy. The code is similar enough for all three tasks that you’d want to build just one place for it, even with three entry points. If you really want to create a complete API experience, often you must constrain a view’s width or height without affecting the other dimension. This expansion is left for you to do as an exercise.

Listing 4-3 Constraining View Size


void constrainViewSize(VIEW_CLASS *view,
CGSize size, NSUInteger priority)
{
NSDictionary *bindings =
NSDictionaryOfVariableBindings(view);
NSDictionary *metrics = @{
@"width":@(size.width),
@"height":@(size.height),
@"priority":@(priority)};

for (NSString *formatString in @[
@"H:[view(==width@priority)]",
@"V:[view(==height@priority)]",
])
{
NSArray *constraints = [NSLayoutConstraint
constraintsWithVisualFormat:formatString
options:0 metrics:metrics views:bindings];
[view addConstraints:constraints];
}
}


Building Rows or Columns

Listing 4-4 demonstrates how to create a row or column from an array of views. You specify an alignment option, a spacer, and a priority. The code determines which axis to use from the alignment you supply. The alignment must be orthogonal to the axis, so if the alignment is horizontal, the layout is vertical or vice versa.

The function in Listing 4-4 builds pairs of constraints, iterating through the views array you pass it. It creates a layout based on both the calculated axis and the spacing constant you provided. For standard spacing, you use @"-", and for no spacing, you use @"". Otherwise, you can pass any string that describes how the spacers should act (for example, @"->=15-").

This function uses the custom install method that was first introduced in Chapter 2, “Constraints.” It installs each constraint safely to its natural destination.

Listing 4-4 Placing Views into a Line


#define IS_HORIZONTAL_ALIGNMENT(ALIGNMENT) \
[@[@(NSLayoutFormatAlignAllLeft), @(NSLayoutFormatAlignAllRight),\
@(NSLayoutFormatAlignAllLeading), @(NSLayoutFormatAlignAllTrailing),\
@(NSLayoutFormatAlignAllCenterX)] containsObject:@(ALIGNMENT)]

void buildLineWithSpacing(NSArray *views, NSLayoutFormatOptions alignment,
NSString *spacing, NSUInteger priority)
{
if (views.count == 0)
return;

VIEW_CLASS *view1, *view2;

// Calculate the axis and its string representation.
// The axis is orthogonal to the requested alignment
// eg, centerX alignment creates a column, and
// trailing builds a row.
BOOL axisIsH = IS_HORIZONTAL_ALIGNMENT(alignment);
NSString *axisString = (axisIsH) ? @"H:" : @"V:";

// Build the format
NSString *format = [NSString
stringWithFormat:@"%@[view1]%@[view2]",
axisString, spacing];

// Apply the format to view pairs
for (int i = 1; i < views.count; i++)
{
view1 = views[i-1];
view2 = views[i];
NSDictionary *bindings =
NSDictionaryOfVariableBindings(view1, view2);
NSArray *constraints = [NSLayoutConstraint
constraintsWithVisualFormat:format options:alignment
metrics:nil views:bindings];
for (NSLayoutConstraint *constraint in constraints)
[constraint install:priority];
}
}


Matching Sizes

Listing 4-5 shows how to use visual formats to match the sizes for all members of a view array. It ensures that you’ve passed at least one view and then matches each of the remaining views to that first one. You specify the axis, and the function applies the visual constraints.

As with Listing 4-4, this function uses the install method from Chapter 2, which is a safe way to install constraints.

Listing 4-5 Matching View Sizes to Each Other


void matchSizes(NSArray *views,
NSInteger axis, NSUInteger priority)
{
if (views.count == 0)
return;

// Create the axis-appropriate format
NSString *format = axis ?
@"V:[view2(==view1@priority)]" :
@"H:[view2(==view1@priority)]";

// Iterate through the views
VIEW_CLASS *view1 = views[0];
for (int i = 1; i < views.count; i++)
{
VIEW_CLASS *view2 = views[i];
NSArray *constraints = [NSLayoutConstraint
constraintsWithVisualFormat:format options:0
metrics:@{@"priority":@(priority)}
views:NSDictionaryOfVariableBindings(view1, view2)];
for (NSLayoutConstraint *constraint in constraints)
[constraint install];
}
}


Why You Cannot Distribute Views

You can’t distribute views along an axis under the current constraint system for equal spacing. Adding a "[A]-(>=0)-[B]-(>=0)-[C]" constraint doesn’t distribute the views along an axis with equal spacing, as you might expect. The basic reason is that each constraint can reference only two views at a time.

An equal spacing or distribution rule must refer to a minimum of three. You can’t say, “The space between View 1 and View 2 equals the space between View 2 and View 3,” nor can you talk about the distances of their leading edges, tops, centers, or other geometries. There aren’t enough references in any NSLayoutConstraint instance to encapsulate (let alone implement) rules like this:

Some distance such that A.center + distance = B.center and B.center + distance = C.center

You also can’t declare simultaneous equations that equate two constraint constant properties, like this:

A. center == B. center + spacer1;
B. center == C. center + spacer2;
spacer1 == spacer2

There is simply no way to express these relations in the y R mx + b format used by the iOS constraint system, where the b offset must be known and relations are only between view attributes. The best you can do is to calculate how far apart you want the items to be and then use a multiplier and offset to manually fix each view’s position.

I do, however, offer two workarounds for your consideration.

How to Pseudo-Distribute Views (Part 1: Equal Centers)

The first of my two solutions works by dividing the superview into N equal sections, where N is the number of views. It co-aligns each view’s center X to the middle of its section.

That’s where a little multiplier hack comes into play. As shown in Listing 4-6, use a function that calculates the percent extent along the superview that reaches to the section’s middle and multiplies it by the “right” attribute, which amounts to the full distance along the view.

Listing 4-6 Distributing Views by Equal Center Placement


void pseudoDistributeCenters(
NSArray *views, NSLayoutFormatOptions alignment,
NSUInteger priority)
{
if (!views.count)
return;

if (alignment == 0)
return;

// Check the alignment for vertical or horizontal placement
BOOL horizontal = IS_HORIZONTAL_ALIGNMENT(alignment);

// The placement is orthogonal to that alignment
NSLayoutAttribute placementAttribute = horizontal ?
NSLayoutAttributeCenterY : NSLayoutAttributeCenterX;
NSLayoutAttribute endAttribute = horizontal ?
NSLayoutAttributeBottom : NSLayoutAttributeRight;

// Cast from NSLayoutFormatOptions to NSLayoutAttribute
NSLayoutAttribute alignmentAttribute =
attributeForAlignment(alignment);

// Iterate through the views
NSLayoutConstraint *constraint;
for (int i = 0; i < views.count; i++)
{
VIEW_CLASS *view = views[i];

// midway across each section
CGFloat multiplier =
((CGFloat) i + 0.5) / ((CGFloat) views.count);

// Install the item position
constraint = [NSLayoutConstraint
constraintWithItem:view
attribute:placementAttribute
relatedBy:NSLayoutRelationEqual
toItem:view.superview
attribute:endAttribute
multiplier:multiplier
constant: 0];
[constraint install:priority];

// Install alignment
constraint = [NSLayoutConstraint
constraintWithItem:views[0]
attribute:alignmentAttribute
relatedBy:NSLayoutRelationEqual
toItem: view
attribute:alignmentAttribute
multiplier:1
constant:0];
[constraint install:priority];
}
}


This function produces a set of views whose centers are equally distributed. The gaps between the views, however, vary by the size of each view. This works best when all views are matched in size, to produce equally spaced results, as shown at the top of Figure 4-13. As the bottom image inFigure 4-13 shows, the results can look a little odd with unmatched sizing. The view-to-view spaces are not even.

Image

Figure 4-13 Distributing centers works best with equal-sized views placed entirely across a shared superview (top). When you distribute centers for unequal-sized views, the spacing becomes visually haphazard (bottom).

This code stretches completely across the parent. If you want to adjust this, you must carefully manipulate the multiplier (to set a different endpoint) and/or constant (to set the starting point). The math is left as an exercise for you.

So, why can’t you use NSLayoutAttributeWidth directly? Unfortunately, you can only relate location attributes to location attributes. You cannot relate them to size attributes. Doing so crashes your application, as shown here:

2013-01-26 23:45:59.739 HelloWorld[16073:c07] *** Terminating app due to uncaught
exception 'NSInvalidArgumentException', reason: '*** +[NSLayoutConstraint
constraintWithItem:attribute:relatedBy:toItem:attribute:multiplier:constant:]: Invalid
pairing of layout attributes'

Pseudo-Distributing Views (Part 2: Spacer Views)

A far better but less elegant solution than the one just shown is to add a series of views between each source view. As you can see in Figure 4-14, it works equally well for same-sized views, views with variable sizes, and even groups whose first and last members have been pinned to odd locations. By adding spacer views, you bypass the “no rules about spaces” problem described earlier. Because your spacers are views themselves, you can add constraint rules that establish the similarity of these views to each other.

Image

Figure 4-14 By creating inter-item spacing views, you can add constraint rules that relate spaces to each other.

Listing 4-7 shows the code that powers this distribution. It builds an array of disposable transparent view spacers. It then adds constraints built like this:

"[view1][spacer(==firstspacer)][view2]"

These constraints add spacers between pairs of views, matching the size of each spacer view. This forces each pair of views to space apart an equal amount.

This distribution strategy involves a few things you need to think about in deployment:

Image First, the layout is underconstrained. This function adds no rules about the placement of the first and last views. So you need to pin those views elsewhere in your code.

Image Second, you must pin both the first and last views. Unlike in Listing 4-4, these spacers are underspecified until after the last view is pinned into position.

Image Third, it uses a lot of extra views that you need to keep track of. This becomes especially troublesome when your design changes between portrait and horizontal orientations and you need to re-layout your views. Each spacer view plays a particular role, and you can’t just reuse spacers arbitrarily. You must track each role, track the spacer that fulfills it, and associate that layout geometry with that particular instance.

Image Fourth, it’s inherently more fragile. You must provide enough space to lay out all your views, especially if you size them with a required priority. If the extent of all your views and spacers is larger than the available width (between the starting edge of the first view and the ending edge of the last view), Auto Layout starts breaking constraints on your behalf.

Work around this fragility by adjusting constraint priorities. For example, if your views can resize to take up the slack, let them do so. If you know your views are equally sized, you can diminish the impact of view resizing by adding a view-size matching rule to the layout format:

"[view1][spacer(==firstspacer)][view2(==view1)]"

Unfortunately, if you work with mixed view sizes, you cannot specify rules that state “resize each view proportionally.” You must accept that some views may shrink significantly and randomly when faced with insufficient space.

Listing 4-7 Adding Spacer Views to Provide Even Distributions


void pseudoDistributeWithSpacers(
VIEW_CLASS *superview, NSArray *views,
NSLayoutFormatOptions alignment, NSUInteger priority)
{
// Must pass views, superview, non-zero alignment
if (!views.count) return;
if (!superview) return;
if (alignment == 0) return;

// Build disposable spacers
NSMutableArray *spacers = [NSMutableArray array];
for (int i = 0; i < views.count; i++)
{
// Create a view, install it, and prepare for autolayout
[spacers addObject:[[VIEW_CLASS alloc] init]];
[spacers[i] setTranslatesAutoresizingMaskIntoConstraints:NO];
[superview addSubview:spacers[i]];
}

BOOL horizontal = IS_HORIZONTAL_ALIGNMENT(alignment);
VIEW_CLASS *firstspacer = spacers[0];

// Structure format
NSString *format = [NSString stringWithFormat:
@"%@: [view1][spacer(==firstspacer)][view2]",
horizontal ? @"V" : @"H"];

// Lay out the row or column
for (int i = 1; i < views.count; i++)
{
VIEW_CLASS *view1 = views[i-1];
VIEW_CLASS *view2 = views[i];
VIEW_CLASS *spacer = spacers[i-1];

// Create bindings
NSDictionary *bindings = NSDictionaryOfVariableBindings(
view1, view2, spacer, firstspacer);

// Build and install constraints
NSArray *constraints = [NSLayoutConstraint
constraintsWithVisualFormat:format
options:alignment metrics:nil views:bindings];
for (NSLayoutConstraint *constraint in constraints)
[constraint install:priority];
}
}


Exercises

After reading this chapter, test your knowledge with these exercises:

1. How many constraints does the format @"H:[view1]-[view2]" produce? How many constraints does it produce if the options parameter is NSLayoutFormatAlignAllBaseline?

2. How many constraints does the format @"H:[view1]" produce? How many constraints does it produce if the options parameter is NSLayoutFormatAlignAllTop?

3. Your format string is @"H:[view1]-[view2]". (a) You pass NSDictionaryOfVariable Bindings(view1, view2, view3, view4, view5) to the views parameter. What happens? (b) You pass NSDictionaryOfVariableBindings(view1, view3)to the views parameter. What happens?

4. How do you request a set of views to align both on the top and on the bottom?

5. How do you request a bottom alignment for a vertical format string, such as @"V:[view1][view2]"?

6. What result does the visual format @"H:|-(50@100)-[view1(==320@200)]-(50@300)-|" produce on a screen that is 320 points wide? On a screen 480 points wide?

7. How wide will this view be: @"H:[view(>=20, <=10)]"?

8. Describe the results the constraint @"H:|-(-20)-[view1(==50)]" produces.

Conclusions

This chapter introduces Auto Layout’s text-based visual formatting language. You discovered how these formats are built and saw many examples that demonstrate their flexibility. Here are a few final thoughts about this technology:

Image Although visual formats are not as nuanced as building straight NSLayoutConstraint instances, they offer the benefit of concise expression, along with self-documenting format strings. Visual formats cover enough ground to match many developers’ light layout needs.

Image While alignment is not a necessary part of constraint creation, it helps condense your code as much as possible. When you set an option with visual formats, you eliminate any need to build separate alignment constraints.

Image All constraints you generate with visual formats have a default 1,000 (required) priority unless you specify otherwise within your text. If you find readability suffering as a result of carefully prioritized layout, consider switching to individual layout constraints for more direct control.

Image Visual constraints are more intuitive to use for layout. Specifying @"H:[nameLabel]-[nameTextField]" is far easier than working out that the text field’s leading edge lies 8 points to the right of the label’s trailing edge, transferring that knowledge to a seven-parameter call, and then testing whether you got the order of the arguments and the sign of the constant right on the first go.

Image Visual formats have an intrinsic flexibility, and there are usually several ways to accomplish your goal. As you read in this chapter, slightly different formats (for example, -0-, -(0)-, -(==0)-) can produce identical constraints, allowing you to customize rules to your personal style.