Chapter    2

Layout Recipes

The user interface and its layout is essential to most applications; especially so in iOS with its orientation-sensitive devices. Users pretty much expect apps of today to support both portrait and landscape orientation. They also expect apps to run on both iPhone and iPad. In other words, they expect your apps to have dynamic layouts.

This chapter shows you how to use Autolayout, a great new way to build dynamic user interfaces in iOS 6.

Recipe 2-1: Using Autolayout

Autolayout is a new feature in iOS 6 that provides a way to handle view layouts in iOS apps. It supersedes the old “springs and struts” layout system, which is still present but now more or less obsolete; all you could do with “springs and struts” can now be done using Autolayout. In addition, Autolayout offers a much more powerful model with which you can build layouts that scale and adapt to screen rotations in a way that was not possible without reverting to code. In this recipe we show you how Autolayout works and how you can start taking advantage of it.

Autolayout Constraints

Autolayout, in its essence, consists of “constraints” dictating the relationship between user interface elements, and a layout engine that enforces the constraints. If you have created a new project in Xcode 4.5, you’ve probably already been using Autolayout and constraints. The feature, which is turned on by default, makes Interface Builder set up constraints automatically for you when you build the user interface. To see how this is done you’re going to create an application with a simple user interface that automatically adapts to both portrait and landscape orientations.

Start by creating a new single-view application. Next build a user interface such as the one in Figure 2-1, using labels, a text field, and a text view.

9781430245995_Fig02-01.jpg

Figure 2-1.  The user interface in portrait orientation

When you position and size the elements, be sure to use the default spacing to the main view’s boundaries as well as between the subviews, i.e., where Interface Builder snaps during drag, as shown in Figure 2-2.

9781430245995_Fig02-02.jpg

Figure 2-2.  The dashed lines in Interface Builder indicate default spacing

While you were building the user interface by positioning and sizing the elements, Interface Builder automatically created the corresponding constraints for you. You can see evidence of these constraints directly in the editor by selecting a view. For example if you select the text field, as in Figure 2-3, you can see that Interface Builder has created four constraints; one for aligning the left edge to the default distance to the main view’s bound, one for aligning to the right, one for aligning the top edge to the bottom edge of the Name label, and one for aligning to the Description label below.

9781430245995_Fig02-03.jpg

Figure 2-3.  A text field with constraints for its left, right, top, and bottom edges

Let’s take a closer look at these constraints. With the text field still selected, go to the Size inspector (in the Utilities View to the right). As shown in Figure 2-4, there’s a section called Constraints that contains the four constraints of the text field. As you can see, there is the constraint for leading space to Superview, trailing space to Superview, top space to the Name label, and bottom space to the Description label; all with default sized spacing.

9781430245995_Fig02-04.jpg

Figure 2-4.  The Size inspector showing the constraints of a text field

Now, open the Attribute inspector for the Leading Space to: constraint by selecting Select and Edit from the Options menu, as shown in Figure 2-5.

9781430245995_Fig02-05.jpg

Figure 2-5.  The Options menu of a constraint

As you can see in Figure 2-6, this constraint is a Horizontal Space Constraint. We’ll talk more about the properties of the constraints later, but for now it’s sufficient to know that the constant (in this case the distance between the left edge of the main view and the left edge of the text field) is set to the Standard spacing.

9781430245995_Fig02-06.jpg

Figure 2-6.  The properties of a Horizontal Space Constraint

Go ahead and explore the rest of the constraints, of the text field and of the other views, so that you get a feeling of how the system sets them up. Be sure not to change any values at this point, as it will most likely confuse you. Later, however, you’ll learn how to customize the constraints to suit your needs.

Now let’s see what the Autolayout system does with these constraints. Build and run your application in the iOS Simulator. When the app has launched, press cmd (image) + right arrow keys to rotate the device to landscape orientation. As you can see from Figure 2-7, the user interface adapts nicely to the new orientation. This is the work of Autolayout. And you didn’t have to do anything to make it work, only lay out your views in Interface Builder as you always do.

9781430245995_Fig02-07.jpg

Figure 2-7.  Autolayout automatically adapts the user interface to landscape and portrait orientations

In contrast, let’s look at how the app behaves with Autolayout turned off. Go to the File Inspector (again located in Xcode’s right Utilities View panel.) and in the Interface Builder Document section (see Figure 2-8) uncheck Use Autolayout.

9781430245995_Fig02-08.jpg

Figure 2-8.  Autolayout is turned on by default for new projects

After unchecking Use Autolayout, build and run the app again. As you can see in Figure 2-9, rotating the device results in a significantly worse user experience.

9781430245995_Fig02-09.jpg

Figure 2-9.  A landscape orientation user interface without Autolayout enabled

Now before moving on, don’t forget to reactivate the Use Autolayout setting by returning to the File Inspector and re-checking Use Autolayout.

Constraint Priorities

You’re probably not overly impressed with the previous Autolayout example; indeed, all that we’ve shown so far can be done using the old layout system. However, what if you wanted a slightly different layout behavior of, for example, the Name text field. What if you didn’t want it to grow beyond a certain width when switching between device orientations. (It’s a common principle in human computer interaction that the size of input fields should reflect the size of expected content.) Let’s say that you want the Name text field to grow when the screen rotates, but only up to 350 pixels. This is where Autolayout starts to shine.

Translated into the language of constraints, you want to add a constraint that states that the text field’s width must be of no more than 350 pixels. But adding this constraint yields a logical problem. When the screen rotates, the system can’t satisfy both the constraint that pins the right edge of the text field, and the one that dictates its maximum width.

What can you do about that? One thought would be to remove the constraint inserted by Interface Builder. However, it doesn’t take much thought to realize that that would leave you with a constraint that’s true for many values, but not decisive enough to settle for one value. No, you need both constraints: you want the right edge of the text field to pin to the right edge of the screen unless that makes the width larger than 350 pixels. The solution is Constraint Priorities.

Autolayout offers the possibility to set a priority value between 0 and 1000 on individual constraints. A value of 1000 means that the constraint is required, but for any other value the constraint with the higher priority takes precedence. In this case, this means that you can make the width constraint required but set a lower priority of the pin to right edge constraint. Then, when the screen rotates, the width constraint will “win” over the other and you will get the effect you seek.

Start by adding the width constraint. You do this by selecting the text field and clicking the Pin button in the Autolayout bar located in the lower-right corner of the interface editor. The Pin button is the one in the middle. Then select the Width constraint, as shown in Figure 2-10.

9781430245995_Fig02-10.jpg

Figure 2-10.  The Autolayout bar in Interface Builder allows you to add your own constraints

Now, in the Attributes Inspector for the new Width Constraint, set the relation to Less Than or Equal and the constant to 350. Leave the priority at 1,000 (required).

9781430245995_Fig02-11.jpg

Figure 2-11.  Setting a Width Constraint to Less Than or Equal to 350

What’s left now is to lower the priority of the trailing space to Superview constraint. Make sure the text field is selected. Then in the Size Inspector, click the Trailing Space to: Superview constraint  and choose Select and Edit from the options menu for that constraint. Change the value of the Priority property to 500, as in Figure 2-12.

9781430245995_Fig02-12.jpg

Figure 2-12.  Lowering priority of a constraint to 500

If you build and run your application now, you will see that when you rotate the device, the text field, as in Figure 2-13, grows but stays at the maximum width of 350.

9781430245995_Fig02-13.jpg

Figure 2-13.  A user interface with a text field that has a Width Constraint of 350 pixels

Adding a Trailing Button

Let’s make things a little more complicated. What if you want to add a Pick a name button on the right side of the text field and still keep the current width constraint? You can accomplish this using Autolayout.

Before jumping in and adding constraints, it’s a good idea to stop and think about the layout in terms of constraints. You want to:

  1. Allow the width of the text field to be less or equal to 350.
  2. Pin the trailing edge of the text field to the leading edge of the button.
  3. Pin the trailing edge of the button to the trailing edge of the screen unless the first constraint is violated.

Note  The reason we’re using the terms Leading and Trailing instead of Left and Right (which are also valid attributes) is that trailing and leading is adapting to changes of text directions in, for example, the Hebrew language. On such a locale, leading becomes right and trailing becomes left, and the user interface adapts accordingly. Yet another reason to use Autolayout.

9781430245995_Fig02-14.jpg

Figure 2-14.  Leading and trailing edges in a left-to-right language locale

Before you add the button to the user interface, it’s important that you reset the priority of the Trailing Space to: Superview constraint and make it make it a required. Otherwise Interface Builder will be confused and set up constraints that you don’t want. So go ahead and select the Trailing Space to: Superview constraint and set its Priority attribute back to 1,000.

Now add a button with the text Pick and position it as in Figure 2-15. Be sure to snap against the standard positions so that the correct constraints are created (according to preceding points 2 and 3.)

9781430245995_Fig02-15.jpg

Figure 2-15.  Adding a button and snapping it against the trailing edges of the text field and the Superview

You should now have constraints corresponding to points 1, 2 and 3. Verify that your text field has the following constraints (order may differ):

  • Width <= 350
  • Top Space to: Label – Name
  • Leading Space to: Superview
  • Bottom Space to: Label – Description
  • Trailing Space to: Button - Pick

And, the button’s constraints:

  • Width Equals: 55
  • Top Space to: Superview
  • Trailing Space to: Superview
  • Align Baseline to: Text Field
  • Leading Space to: Text Field

Caution  If your list of constraints differ from the preceding list, delete the constraints (that are allowed by the system to be removed) and reposition the controls. Interface Builder then re-creates the constraints you want.

What’s missing now is to lose the constraint of the button’s trailing edge. In the same way as previously, select the button and the Trailing Space to: Superview constraint. Change its Priority attribute to 500.

Build and run. When rotated, the user interface should adapt nicely and keep the text field at the maximum width, while the button stays pinned to its right, as shown in Figure 2-16.

9781430245995_Fig02-16.jpg

Figure 2-16.  A user interface with a text field that has a Width Constraint of 350 pixels and a button trailing to its right

Although simple, we hope that the example here has opened your eyes to the possibilities of Autolayout. The next recipe takes it to the next level, where you’ll create constraints from code, constructing a truly dynamic layout.

Recipe 2-2: Programming Autolayout

The preferred way to setup Autolayout constraints is to use Interface Builder (as shown in Recipe 2.1.) The main reason being that Interface Builder won’t let you setup constraints that are unsatisfiable or ambiguous. For example, you can’t remove or change existing constraints in such a way that violates correctness. This can be a frustrating experience, but it forces you to learn Autolayout up front and not in time-consuming debug sessions (which is worse).

However, there are situations where you can’t define your Autolayout constraints in Interface Builder, for example if you create the user interface components dynamically in code. For these situations you need to revert to code for setting up the constraints. In this recipe we’ll show you how.

Setting Up the Application

You’re going to build a simple app that has three buttons; one for adding a new image view to the screen, one for removing the last image view added, and one for removing all added image views. You’re going to use Autolayout to position the buttons in a row at the top of your screen. You also use Autolayout to position the image views so that they overlap each other with the last added image view on top, and each image view has a ten percent increase in size compared to the previous.

Start by creating a new single-view application and then add the following properties to its view controller:

//
//  ViewController.h
//  Recipe 2.2: Coding Autolayout
//

#import <UIKit/UIKit.h>

@interface ViewController : UIViewController

@property (strong, nonatomic) UIButton *addButton;
@property (strong, nonatomic) UIButton *removeButton;
@property (strong, nonatomic) UIButton *clearButton;
@property (strong, nonatomic) NSMutableArray *imageViews;
@property (strong, nonatomic) NSMutableArray *imageViewConstraints;

@end

You’re going to create the three buttons directly in code using a helper method to reduce code duplication. Switch to ViewController.m and add the following code:

- (UIButton *)addButtonWithTitle:(NSString *)title action:(SEL)selector
{
    UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    [button setTitle:title forState:UIControlStateNormal];
    [button addTarget:self action:selector forControlEvents:UIControlEventTouchUpInside];
    button.translatesAutoresizingMaskIntoConstraints = NO;
    [self.view addSubview:button];
    return button;
}

The method creates a new Round Rect Button with the provided title and action method. What’s notable here is the setting of the translatesAutoresizingMaskIntoConstraints property of the button to NO. This is important to do if you’re defining your own Autolayout constraints, otherwise you’re likely to end up with conflicting constraints. Recipe 2-3 has more to say on this subject.

Note  You don’t explicitly set view frames when using Autolayout. Instead, a view’s position and size are dictated by the constraints you define.

With the helper method in place you can turn to the viewDidLoad method and add code to create the buttons:

- (void)viewDidLoad
{
    [super viewDidLoad];
    self.addButton = [self addButtonWithTitle:@"Add" action:@selector(addImageView)];
    self.removeButton = [self addButtonWithTitle:@"Remove" action:@selector(removeImageView)];
    self.clearButton = [self addButtonWithTitle:@"Clear" action:@selector(clearImageViews)];
}

Next, add stubs for the three action methods now connected to the respective button. You’ll implement these methods later, but leave them empty for now:

- (void)addImageView
{
}
- (void)removeImageView
{
}
- (void)clearImageViews
{
}

If you run your application now, you would see nothing but a gray screen. The buttons wouldn’t show because you haven’t yet defined any Autolayout constraints to dictate their positions and sizes. So let’s go ahead and do that.

There are two principle ways to create constraints from code. You can either use the so called Visual Format Language, or you can use the constraintWithItem:attribute:relatedBy:toItem:attribute:multiplier:constant: method. The former has the advantage that it provides better visualization of the constraints created; the latter, on the other hand, provides completeness (not all constraints can be expressed using the Visual Format Language).

Often you’ll mix the two ways to create your constraints. In this case you use the format language for positioning the buttons, and a mix of the two for placing the image views.

The Visual Format Language

Before moving on and starting to create the constraints, let’s take a quick look at the Visual Format Language. For example, this string defines constraints that position button2 right next to button1 with a spacing of 20 pixels between the two:

[button1]-20-[button2]

A single hyphen indicates default spacing:

[button1]-[button2]

Here are same constraints but for vertical layout:

V:[button1]-[button2]

Although horizontal is the default, you can explicitly state it:

H:[button1]-[button2]

The spacing toward the Superview is indicated with a | character. The following example states that textField should be pinned to both leading and trailing ends of the Superview, with a default spacing:

|-[textField]-|

You can also define a component size. This example says that button1 shall be 50 pixels wide and that button2 has the same width as button1:

[button1(50)]-[button2(==button1)]

You also can have inequalities, as in the following example, which states that button1 shall be at least 50 pixels wide:

[button1(>=50)]

You can set both a minimum and a maximum width at the same time:

[button1(>=50, <=100)]

You also can set priorities on the size constraints, for example that button1 shall be at least 50 pixels wide, but with a priority at 500, which makes it non-required but desirable:

[button1(>=50@500)

Table 2-1 shows the syntax elements and some additional examples of the Visual Format Language.

Table 2-1. Visual Format Language Syntax Elements

Syntax Elements Examples Description
H:, V: H:|-[statusLabel]-| Horizontal or vertical orientation. Default orientation is horizontal so the H: can therefore be omitted.
V:|[textView]|
| |[textView]| Indicates the Superview; its leading end if on the left side and trailing on the right
- [button1]-[button2] Standard space
-N- |-20-[view] An N-sized spacing
[view] Indicates a subview
==, >=, <= [view1(==view2)] Relation operators. Can only be used in size constraints
[view( >= 30, <=100)]
@N [view(==50@500)] Constraint priority. Can only be used in size constraints. Default priority is 1000 (i.e., a required constraint)
[view1(==view2@500, >= 30)]

Now, let’s add constraints that position the three buttons in a row at the top of the screen. Because these constraints are always the same, you add them directly in the viewDidLoad method. You start by creating a dictionary containing the buttons with identifying keys. Autolayout uses the dictionary to map identifiers in the format language strings to the corresponding views (buttons in this case):

NSDictionary *viewsDictionary =
    [[NSDictionary alloc] initWithObjectsAndKeys:
        self.addButton, @"addButton",
        self.removeButton, @"removeButton",
        self.clearButton, @"clearButton", nil];

Then you add the constraints that pin the buttons to each other in a row. You do this by calling addConstraints:constraintsWithVisualFormat:options:metrics:views: method of the main view, providing the visual format string (marked in bold here):

[self.view addConstraints:[NSLayoutConstraint
    constraintsWithVisualFormat:@"H:|-[addButton]-[removeButton]-[clearButton]"
    options:0 metrics:nil views:viewsDictionary]];

Next, you pin the buttons to the top of the screen:

[self.view addConstraints:[NSLayoutConstraint
    constraintsWithVisualFormat:@"V:|-[addButton]"
    options:0 metrics:nil views:viewsDictionary]];
[self.view addConstraints:[NSLayoutConstraint
    constraintsWithVisualFormat:@"V:|-[removeButton]"
    options:0 metrics:nil views:viewsDictionary]];
[self.view addConstraints:[NSLayoutConstraint
    constraintsWithVisualFormat:@"V:|-[clearButton]"
    options:0 metrics:nil views:viewsDictionary]];

Here’s what the viewDidLoad looks like at this point:

- (void)viewDidLoad
{
    [super viewDidLoad];
    self.addButton = [self addButtonWithTitle:@"Add" action:@selector(addImageView)];
    self.removeButton = [self addButtonWithTitle:@"Remove" action:@selector(removeImageView)];
    self.clearButton = [self addButtonWithTitle:@"Clear" action:@selector(clearImageViews)];

    NSDictionary *viewsDictionary =
        [[NSDictionary alloc] initWithObjectsAndKeys:
            self.addButton, @"addButton",
            self.removeButton, @"removeButton",
            self.clearButton, @"clearButton", nil];
     
[self.view addConstraints:[NSLayoutConstraint
        constraintsWithVisualFormat:@"H:|-[addButton]-[removeButton]-[clearButton]"
        options:0 metrics:nil views:viewsDictionary]];
    [self.view addConstraints:[NSLayoutConstraint
        constraintsWithVisualFormat:@"V:|-[addButton]"
        options:0 metrics:nil views:viewsDictionary]];
    [self.view addConstraints:[NSLayoutConstraint
        constraintsWithVisualFormat:@"V:|-[removeButton]"
        options:0 metrics:nil views:viewsDictionary]];
    [self.view addConstraints:[NSLayoutConstraint
        constraintsWithVisualFormat:@"V:|-[clearButton]"
        options:0 metrics:nil views:viewsDictionary]];

}

You now can build and run your application. Your screen should look like the one in Figure 2-17.

9781430245995_Fig02-17.jpg

Figure 2-17.  A row of buttons positioned using Autolayout

Now that you’ve verified that the layout is as expected, it’s time to implement the respective button’s action method. We’ll start with the adding of image views.

Adding Image Views

Before you move on and start implementing the addImageView action method, you need to do a little more setup and initialization. First, you need an image that you’re going to populate the image views with. (For simplicity we use the same image for all image views.) Therefore, using a Finder window, drag an image of your liking (and which is in the PNG format) to your Supporting Files folder of your project. (Chapter 1 contains a detailed description on how to add resource files, such as images, to an Xcode project.) In the following code, be sure to replace the name "sweflag" which is the name of the image we chose, to the name of your image file.

Next,  initialize the two array properties that you added to the view controller’s header file in the beginning of this recipe. Add the following code to the viewDidLoad method. The code for creating the three buttons and their constraints has been removed for brevity:

- (void)viewDidLoad
{
    [super viewDidLoad];
    // ...
    self.imageViews = [[NSMutableArray alloc]initWithCapacity:10];
    self.imageViewConstraints = [[NSMutableArray alloc]initWithCapacity:10];

}

Now implement the addImageView action method:

- (void)addImageView
{
    UIImage *image = [UIImage imageNamed:@"sweflag"];
    UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
    [self.view addSubview:imageView];
    imageView.translatesAutoresizingMaskIntoConstraints = NO;
    [self.imageViews addObject:imageView];

    [self rebuildImageViewsConstraints];

}

The implementation is straightforward. It creates and adds an image view to the main view, sets its translatesAutoresizingMaskIntoConstraints to NO, so as to not conflict with the custom constraints you’ll set up in a minute, adds itself to the imageViews array for later reference, and finally orders a rebuild of all the image views’ constraints.

Defining the Image Views’ Constraints

The rebuildImageViewsConstraints helper method removes all previous image view constraints and re-creates them. Here’s the basic structure without the actual creating of the constraints:

- (void)rebuildImageViewsConstraints
{
    [self.view removeConstraints:self.imageViewConstraints];
    [self.imageViewConstraints removeAllObjects];
    if (self.imageViews.count == 0)
        return;
    // TODO: Build the imageViewConstraints array
    [self.view addConstraints:self.imageViewConstraints];
}

To build the imageViewConstraints array (beginning at the code comment in the prior code block) you start with the constraints that pin the first image view to the left side of the screen and right below the row of buttons. At the same time you set the first image view’s size to 50 by 50 pixels. Here’s the code:


UIImageView *firstImageView = [self.imageViews objectAtIndex:0];

NSDictionary *viewsDictionary = [[NSDictionary alloc]
    initWithObjectsAndKeys:self.addButton, @"firstButton", firstImageView, @"firstImageView", nil];

// Pin first view to the top left corner
[self.imageViewConstraints addObjectsFromArray:[NSLayoutConstraint
    constraintsWithVisualFormat:@"H:|-[firstImageView(50)]"
    options:0 metrics:nil views:viewsDictionary]];

[self.imageViewConstraints addObjectsFromArray:[NSLayoutConstraint
    constraintsWithVisualFormat:@"V:[firstButton]-[firstImageView(50)]"
    options:0 metrics:nil views:viewsDictionary]];

Each of the remaining image views (if any) overlap the previous one with an offset of 10 pixels to the right and down. Additionally, for effects, each image is ten percent bigger than the previous. In pseudo code, what you want to do is something like this:

imageView(N).X = imageView(N-1).X +  10;
imageView(N).Y = imagView(N-1).Y + 10;
imageView(N).Width = imageView(N-1).Width * 1.1;
imageView(N).Height = imageView(N-1).Height * 1.1;

Unfortunately, this is nothing you can translate to Autolayout constraints using the visual format notation. Instead you create the corresponding constraints explicitly, like so:

if (self.imageViews.count > 1)
{
    UIImageView *previousImageView = firstImageView;
    for (int i = 1; i < self.imageViews.count; i++)
    {
        UIImageView *imageView = [self.imageViews objectAtIndex:i];
        [self.imageViewConstraints addObject:[NSLayoutConstraint
             constraintWithItem:imageView attribute:NSLayoutAttributeLeading
             relatedBy:NSLayoutRelationEqual
             toItem:previousImageView attribute:NSLayoutAttributeLeading
             multiplier:1 constant:10]];

        [self.imageViewConstraints addObject:[NSLayoutConstraint
             constraintWithItem:imageView attribute:NSLayoutAttributeTop
             relatedBy:NSLayoutRelationEqual
             toItem:previousImageView attribute:NSLayoutAttributeTop
             multiplier:1 constant:10]];

        [self.imageViewConstraints addObject:[NSLayoutConstraint
            constraintWithItem:imageView attribute:NSLayoutAttributeWidth
            relatedBy:NSLayoutRelationEqual
            toItem:previousImageView attribute:NSLayoutAttributeWidth
            multiplier:1.1 constant:0]];

        [self.imageViewConstraints addObject:[NSLayoutConstraint
            constraintWithItem:imageView attribute:NSLayoutAttributeHeight
            relatedBy:NSLayoutRelationEqual
            toItem:previousImageView attribute:NSLayoutAttributeHeight
            multiplier:1.1 constant:0]];

        previousImageView = imageView;
    }
}

Note  The Visual Format Language of Autolayout was designed for readability over completeness. Therefore you cannot use it to express special cases like overlaps and multiplied property references. For these cases you have to create the constraints explicitly using the constraintWithItem:attribute:relatedBy:toItem:attribute:multiplier:constant: method.

Here’s the complete rebuildImageViewsConstraints method:

- (void)rebuildImageViewsConstraints
{
    [self.view removeConstraints:self.imageViewConstraints];
    [self.imageViewConstraints removeAllObjects];

   
if (self.imageViews.count == 0)
        return;

    UIImageView *firstImageView = [self.imageViews objectAtIndex:0];

    // Pin first view to the top left corner
    NSDictionary *viewsDictionary =
        [[NSDictionary alloc] initWithObjectsAndKeys:
            self.addButton, @"firstButton",
            firstImageView, @"firstImageView", nil];

    [self.imageViewConstraints addObjectsFromArray:[NSLayoutConstraint
        constraintsWithVisualFormat:@"H:|-[firstImageView(50)]"
        options:0 metrics:nil views:viewsDictionary]];

    [self.imageViewConstraints addObjectsFromArray:[NSLayoutConstraint
        constraintsWithVisualFormat:@"V:[firstButton]-[firstImageView(50)]"
        options:0 metrics:nil views:viewsDictionary]];

    if (self.imageViews.count > 1)
    {
        UIImageView *previousImageView = firstImageView;

        for (int i = 1; i < self.imageViews.count; i++)
        {
            UIImageView *imageView = [self.imageViews objectAtIndex:i];

            [self.imageViewConstraints addObject:[NSLayoutConstraint
                constraintWithItem:imageView attribute:NSLayoutAttributeLeading
                relatedBy:NSLayoutRelationEqual
                toItem:previousImageView attribute:NSLayoutAttributeLeading
                multiplier:1 constant:10]];

            [self.imageViewConstraints addObject:[NSLayoutConstraint
                constraintWithItem:imageView attribute:NSLayoutAttributeTop
                relatedBy:NSLayoutRelationEqual
                toItem:previousImageView attribute:NSLayoutAttributeTop
                multiplier:1 constant:10]];

            [self.imageViewConstraints addObject:[NSLayoutConstraint
                constraintWithItem:imageView attribute:NSLayoutAttributeWidth
                relatedBy:NSLayoutRelationEqual
                toItem:previousImageView attribute:NSLayoutAttributeWidth
                multiplier:1.1 constant:0]];

            [self.imageViewConstraints addObject:[NSLayoutConstraint
                constraintWithItem:imageView attribute:NSLayoutAttributeHeight
                relatedBy:NSLayoutRelationEqual
                toItem:previousImageView attribute:NSLayoutAttributeHeight
                multiplier:1.1 constant:0]];

            previousImageView = imageView;
        }
    }
    [self.view addConstraints:self.imageViewConstraints];
}

The only thing remaining now is to implement the action methods for removing the last image view and removing all image views:

- (void)removeImageView
{
    if (self.imageViews.count > 0)
    {
        [self.imageViews.lastObject removeFromSuperview];
        [self.imageViews removeLastObject];
        [self rebuildImageViewsConstraints];
    }
}
- (void)clearImageViews
{
    if (self.imageViews.count > 0)
    {
        for (int i = self.imageViews.count - 1; i >= 0; i--)
        {
            UIImageView *imageView = [self.imageViews objectAtIndex:i];
            [imageView removeFromSuperview];
            [self.imageViews removeObjectAtIndex:i];
        }
        [self rebuildImageViewsConstraints];
    }
}

Your app is now finished and if you build and run it, you should be able to repeatedly add and remove image views using the three buttons. Figure 2-18 shows an example in which we’ve added several image views.

9781430245995_Fig02-18.jpg

Figure 2-18.  Overlapping image views positioned using Autolayout

As Figure 2-19 shows, thanks to Autolayout the app works equally well in landscape orientation.

9781430245995_Fig02-19.jpg

Figure 2-19.  Autolayout automatically adjusts the layout when rotated into landscape orientation

Recipe 2-3: Debugging Autolayout

Dealing with Autolayout can be quite difficult, especially when you’re new to it. The general advice here is to think carefully about the constraints before you start coding them. However, to minimize the time spent in trial-and-error mode it’s important to know what’s actually wrong with a problematic layout; i.e., you need to know how to debug it.

Besides syntax errors in Visual Format strings there are two major ways in which an Autolayout may fail. The first is unsatisfiability, that is, two or more constraints conflicting with each other in such a way that the layout engine can’t simultaneously satisfy them. The second way is caused by ambiguity. That happens when the defined constraints aren’t specific enough, leaving the layout engine with several possible values for a property.

In this recipe we show you examples of both ambiguous and unsatisfiable constraints. We’ll show you how to identify them and how to tackle them.

Dealing with Ambiguous Layouts

To get started you need a new project, so go ahead and create one using the single-view template. Start with a simple case of an ambiguous layout. Let’s say you want to programmatically add three buttons of equal size to the top of the screen. In the view controller’s viewDidLoad method, add the following code to create the buttons and add them to the main view:

- (void)viewDidLoad
{
    [super viewDidLoad];

    UIButton *button1 = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    [button1 setTitle:@"Button 1" forState:UIControlStateNormal];
    button1.translatesAutoresizingMaskIntoConstraints = NO;
    [self.view addSubview:button1];

    UIButton *button2 = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    [button2 setTitle:@"Button 2" forState:UIControlStateNormal];
    button2.translatesAutoresizingMaskIntoConstraints = NO;
    [self.view addSubview:button2];

    UIButton *button3 = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    [button3 setTitle:@"Button 3" forState:UIControlStateNormal];
    button3.translatesAutoresizingMaskIntoConstraints = NO;
    [self.view addSubview:button3];


}

Then add constraints to pin the buttons to the top of the screen:

- (void)viewDidLoad

{
    [super viewDidLoad];

    // ...

    NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(button1, button2, button3);

    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[button1]"
        options:0 metrics:nil views:viewsDictionary]];
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[button2]"
        options:0 metrics:nil views:viewsDictionary]];
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[button3]"
        options:0 metrics:nil views:viewsDictionary]];

}

Note  NSDictionaryOfVariableBindings() is a convenience function for creating the dictionary needed by NSLayoutConstraint to map identifiers in your visual format strings to the views. It creates entries for the provided views using the variable names as keys.

Finally, add constraints for the horizontal layout, pinning the buttons to each other and to the bounds of the screen:

- (void)viewDidLoad
{

    [super viewDidLoad];

    // ...

    NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(button1, button2, button3);
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[button1]"
        options:0 metrics:nil views:viewsDictionary]];
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[button2]"
        options:0 metrics:nil views:viewsDictionary]];
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[button3]"
        options:0 metrics:nil views:viewsDictionary]];

    [self.view addConstraints:[NSLayoutConstraint
        constraintsWithVisualFormat:@"|-[button1]-[button2]-[button3]-|"
        options:0 metrics:nil views:viewsDictionary]];

}

Build and run the app, expecting to see your buttons in a nice row at the top of the screen. The buttons show up, but when you rotate the screen, as shown in Figure 2-20, one of the buttons gets significantly wider than the other two.

9781430245995_Fig02-20.jpg

Figure 2-20.  Due to ambiguous constraints, one of the buttons is wider than the other two

What’s going on here? Your first thought when something like this happens is that you might have ambiguous constraints. To verify that that’s the case, leave the app running but go back to Xcode and press the Pause Program Execution button (see Figure 2-21) in the Debug Area toolbar.

9781430245995_Fig02-21.jpg

Figure 2-21.  The Pause Program Execution button in Xcode

With the program paused, you can then use the (lldb) prompt to enter the following command:

po [[UIWindow keyWindow] _autolayoutTrace]

You then get a trace showing that the three buttons indeed have ambiguous layouts (see Figure 2-22).

9781430245995_Fig02-22.jpg

Figure 2-22.  An Autolayout trace indicating ambiguous layouts

Note  po (or print-object) is a debugger command that prints out the description text of an object. It can be a very useful tool when debugging your application.

So what’s the problem? Usually when it comes to ambiguous layouts it’s a sign that you’re missing one or more constraints. The problem in this case is that you haven’t specified the widths of the buttons enough. All you’ve said is that the buttons should be pinned to each other and to the edges of the screen, so when the size of the screen increases, the layout engine has several options: It can increase the width of the first button, or it can increase the width of the second button, and so on.

What you want, though, is to have buttons of equal widths. So to solve the problem. just add constraints saying that button2 and button3 are of the same width as button1, like so:

- (void)viewDidLoad
{

    [super viewDidLoad];
    // ...

    NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(button1, button2, button3);
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[button1]"
        options:0 metrics:nil views:viewsDictionary]];
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[button2]"
        options:0 metrics:nil views:viewsDictionary]];

    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[button3]"
        options:0 metrics:nil views:viewsDictionary]];
    [self.view addConstraints:[NSLayoutConstraint
        constraintsWithVisualFormat:@"|-[button1]-[button2(==button1)]-[button3(==button1)]-|"
        options:0 metrics:nil views:viewsDictionary]];
}

This leaves the layout engine with only one option: to increase the widths equally for all three buttons. So now when you build and run you’ll get the expected result as in Figure 2-23.

9781430245995_Fig02-23.jpg

Figure 2-23.  A user interface with constraints specifying the buttons to be of equal width

Handling Unsatisfiability

The opposite of ambiguous constraints is unsatisfiable constraints. In those cases, the probable cause is not too few, but too many constraints. This can be a little more tricky to solve because you’ve added those constraints for a purpose. Therefore the unsatisfiability might be a sign that you’ve made logical errors and need to rethink the whole layout.

However, let’s start with a simple yet common mistake that occurs in unsatisfiable constraints. Let’s say you forgot to set the translatesAutoresizingMaskIntoConstraints to NO for one of your buttons. That usually ends up in conflicting constraints between the ones the framework adds (for the autoresizing mask) and your own.

To see what happens, comment out the following row that sets the translatesAutoresizingMaskIntoConstraints property of your third button:

- (void)viewDidLoad
{
    [super viewDidLoad];

    // ...

    UIButton *button3 = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    [button3 setTitle:@"Button 3" forState:UIControlStateNormal];
    // button3.translatesAutoresizingMaskIntoConstraints = NO;
    [self.view addSubview:button3];

    // ...

}

If you build and run now, you’ll see that the buttons seem to have disappeared from the screen, as in Figure 2-24.

9781430245995_Fig02-24.jpg

Figure 2-24.  Forgetting to turn off the automatic creation of autoresizing mask constraints can end up in unsatisfiable constraints errors like this

If you look in the error log, you’ll see a long error text starting with the reason for the failure:

2012-08-07 12:49:34.504 Testing Debugging Constraints[17898:11303] Unable to simultaneously satisfy constraints.

Further down the log message you’ll find a list of the constraints involved, and also a hint about what may be going on:

(Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints)
(
    " < NSAutoresizingMaskLayoutConstraint:0x7528980 h = −−& v = −−& UIRoundedRectButton:0x71327f0.midX == > ",
    " < NSLayoutConstraint:0x7134d60 H:[UIRoundedRectButton:0x7132100]-(NSSpace(8))-[UIRoundedRectButton:0x71327f0] > ",
    " < NSLayoutConstraint:0x7134da0 UIRoundedRectButton:0x71327f0.width == UIRoundedRectButton:0x712e150.width > ",
    " < NSLayoutConstraint:0x7134ce0 UIRoundedRectButton:0x7132100.width == UIRoundedRectButton:0x712e150.width > ",
    " < NSLayoutConstraint:0x7134c10 H:[UIRoundedRectButton:0x712e150]-(NSSpace(8))-[UIRoundedRectButton:0x7132100] > ",
    " < NSLayoutConstraint:0x7134bb0 H:|-(NSSpace(20))-[UIRoundedRectButton:0x712e150]   (Names: '|':UIView:0x712e760 ) > "
)

Indeed, you seem to have NSAutoresizingMaskLayoutConstraints associated with one of your buttons. So there’s your problem. Uncomment the row you previously commented out and re-run your application. It should now work as previously.

Now, let’s create another example of an unsatisfiable layout. Let’s say you want to change the layout from the previous section (the one with three buttons) so that the button widths don’t grow beyond 100 pixels wide when the screen rotates. Add the following width constraint:

[self.view addConstraints:[NSLayoutConstraint
   constraintsWithVisualFormat:@"|-[button1(<=100)]-[button2(==button1)]-[button3(==button1)]-|"
   options:0 metrics:nil views:viewsDictionary]];

You build and run and all looks great in portrait mode, but when you rotate the screen, a strange thing happens. As Figure 2-25 shows, the first two buttons get aligned to the left, while the third button is pinned to the right side of the screen.

9781430245995_Fig02-25.jpg

Figure 2-25.  Casually adding a width constraint can have unexpected results

Again, the error log indicates that you are dealing with unsatisfiable constraints. Let’s take a closer look at the involved constraints:

(
    " <NSLayoutConstraint:0xff4eb60 H:[UIRoundedRectButton:0xff4c510]-(NSSpace(20))-|   (Names: '|':UIView:0xff48470 )> ",
    " <NSLayoutConstraint:0xff4eae0 H:[UIRoundedRectButton:0xff4be20]-(NSSpace(8))-[UIRoundedRectButton:0xff4c510]> ",
    " <NSLayoutConstraint:0xff4eb20 UIRoundedRectButton:0xff4c510.width == UIRoundedRectButton:0xff47e40.width> ",
    " <NSLayoutConstraint:0xff4ea70 UIRoundedRectButton:0xff4be20.width == UIRoundedRectButton:0xff47e40.width> ",
    " <NSLayoutConstraint:0xff4ea30 H:[UIRoundedRectButton:0xff47e40]-(NSSpace(8))-[UIRoundedRectButton:0xff4be20]> ",
    " <NSLayoutConstraint:0xff4e9e0 H:[UIRoundedRectButton:0xff47e40(<=100)]> ",
    " <NSLayoutConstraint:0xff4e8f0 H:|-(NSSpace(20))-[UIRoundedRectButton:0xff47e40]   (Names: '|':UIView:0xff48470 )> ",
    " <NSAutoresizingMaskLayoutConstraint:0x7552280 h = −−- v = −−- V:[UIWindow:0x71a5060(480)]> ",
    " <NSAutoresizingMaskLayoutConstraint:0x71a8870 h = −&- v = −&- UIView:0xff48470.height == UIWindow:0x71a5060.height> "
)

The two NSAutoresizingMaskLayoutConstraint entries are associated with the main view and are okay (you shouldn’t turn the translatesAutoresizingMaskIntoConstraints for root views). But the others give clues of what’s going on. The problem here is that you have pinned the group of buttons to the screen edges. So when the screen rotates, the button widths will grow beyond 100 pixels and the layout engine can’t satisfy the width constraint you added.

What you need to do here is to rethink the layout. What do you want?

  1. Buttons of equal widths
  2. Buttons positioned next to each other, with the default spacing.
  3. The left and right buttons pinned to the respective screen edge, unless that causes the button widths to grow beyond 100 pixels. In that case, you want the group of buttons to stay centered in the screen.

The key here is in the third point where “unless” indicates that you should use non-required constraints. But  start with the first two points. They can be expressed in the same visual format string:

 [self.view addConstraints:[NSLayoutConstraint
    constraintsWithVisualFormat:@"[button1(<=100)]-[button2(==button1)]-[button3(==button1)]"
    options:0 metrics:nil views:viewsDictionary]];

Note  You may be wondering why we, in the above format string, don’t also pin button1 to the left edge of the screen, and button3 to the right. The reason is that those constraints should be non-required, but in the visual format language you can only set priorities (for example, making them non-required) for size constraints. It’s not possible to set priorities for constraints that operate on properties like leading and trailing edges.

Next, you want to loosely pin the group of buttons to the screen edges (with a 20-pixel spacing):

NSLayoutConstraint *pinToLeft =
    [NSLayoutConstraint
        constraintWithItem:button1 attribute:NSLayoutAttributeLeading
        relatedBy:NSLayoutRelationEqual
        toItem:self.view attribute:NSLayoutAttributeLeading
        multiplier:1 constant:20];
pinToLeft.priority = 500;
[self.view addConstraint:pinToLeft];

NSLayoutConstraint *pinToRight =
    [NSLayoutConstraint
        constraintWithItem:button3 attribute:NSLayoutAttributeTrailing
        relatedBy:NSLayoutRelationEqual
        toItem:self.view attribute:NSLayoutAttributeTrailing
        multiplier:1 constant:20];
pinToRight.priority = 500;
[self.view addConstraint:pinToRight];

Finally, you need the rule that tells the group to center in the screen. This can be a required constraint because it is true even if the group is pinned to the screen edges:

NSLayoutConstraint *center =
    [NSLayoutConstraint
        constraintWithItem:button2 attribute:NSLayoutAttributeCenterX
        relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeCenterX
         multiplier:1 constant:0];
[self.view addConstraint:center];

Here’s the resulting viewDidLoad method, changes marked in bold:

- (void)viewDidLoad
{
    [super viewDidLoad];

    UIButton *button1 = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    [button1 setTitle:@"Button 1" forState:UIControlStateNormal];
    button1.translatesAutoresizingMaskIntoConstraints = NO;
    [self.view addSubview:button1];

    UIButton *button2 = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    [button2 setTitle:@"Button 2" forState:UIControlStateNormal];
    button2.translatesAutoresizingMaskIntoConstraints = NO;
    [self.view addSubview:button2];

    UIButton *button3 = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    [button3 setTitle:@"Button 3" forState:UIControlStateNormal];
    button3.translatesAutoresizingMaskIntoConstraints = NO;
    [self.view addSubview:button3];

    NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(button1, button2, button3);

    [self.view addConstraints:
     [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[button1]" options:0 metrics:nil views:viewsDictionary]];

    [self.view addConstraints:
     [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[button2]" options:0 metrics:nil views:viewsDictionary]];
    [self.view addConstraints:
     [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[button3]" options:0 metrics:nil views:viewsDictionary]];
    [self.view addConstraints:[NSLayoutConstraint
       constraintsWithVisualFormat:@"[button1(<=100)]-[button2(==button1)]-[button3(==button1)]"
       options:0 metrics:nil views:viewsDictionary]];

    NSLayoutConstraint *pinToLeft = [NSLayoutConstraint
        constraintWithItem:button1 attribute:NSLayoutAttributeLeading
        relatedBy:NSLayoutRelationEqual
        toItem:self.view attribute:NSLayoutAttributeLeading
        multiplier:1 constant:20];
    pinToLeft.priority = 500;
    [self.view addConstraint:pinToLeft];
    NSLayoutConstraint *pinToRight = [NSLayoutConstraint
        constraintWithItem:button3 attribute:NSLayoutAttributeTrailing
        relatedBy:NSLayoutRelationEqual
        toItem:self.view attribute:NSLayoutAttributeTrailing
        multiplier:1 constant:20];
    pinToRight.priority = 500;
    [self.view addConstraint:pinToRight];
    NSLayoutConstraint *center = [NSLayoutConstraint
        constraintWithItem:button2 attribute:NSLayoutAttributeCenterX
        relatedBy:NSLayoutRelationEqual
        toItem:self.view attribute:NSLayoutAttributeCenterX
        multiplier:1 constant:0];
    [self.view addConstraint:center];

}

Now you can build and run your application again. It should look as before in portrait orientation, but when rotated to landscape there should be no errors showing up in the error log, and the user interface should look like you wanted it, as in Figure 2-26.

9781430245995_Fig02-26.jpg

Figure 2-26.  A layout with maximum button widths correctly set

Summary

In this chapter you learned the basics of Autolayout and how to use it to build dynamic user interfaces that adapt to changes in screen size and orientation. You set up constraints in Interface Builder, as well as in code. You also looked at the two error states, ambiguous constraints and unsatisfiable constraints, and how to debug them.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset