iOS 7:Auto Layout Tutorial in (Part 2)

Note from Ray:Tutorial Team member Matthijs Hollemans (the iOS Apprentice Series author) has ported this tutorial to iOS 7 as part of theiOS 7 feast. We hope you enjoy!

Inpart 1 of this Auto Layout tutorialyou saw that the old “struts-and-springs” model for making user interfaces cannot easily solve all layout problems. Auto Layout is the solution, but because this technology is so powerful it is also a bit more tricky to use.

Thankfully, Xcode 5 makes Auto Layout a lot easier. If you tried Auto

Layout in Xcode 4 and gave up, then we invite you to give it another

try with Xcode 5.

In this second part and final part of the Auto Layout tutorial series, you’ll continue learning all about constraints and how to apply them!

A little runtime excursion

This Auto Layout tutorial begins with a very simple app that looks like this:

It has two buttons that have their background color set just so it’s clearer to see their boundaries. The buttons have a number of constraints between them. If you’ve been following along with theprevious partyou can continue using your existing app. Simply remove the other two buttons from the canvas.

If you’re starting from scratch, create a new iPhone application using theSingle View Applicationtemplate. Drag two buttons into the scene and give them a background color. Use theEditor\Pinmenu to make a Vertical Spacing constraint between the two buttons (40 points), and a Bottom Space to Superview constraint on the lower button (20 points). Use theEditor\Alignmenu to center the yellow button horizontally in the container, and again to align the left edges of both buttons.

Playing with this in Interface Builder is all well and good, but let’s see how this works at runtime. Add the following method toViewController.m:

-(IBAction)buttonTapped:(UIButton*)sender{if([[sender titleForState:UIControlStateNormal]isEqualToString:@"X"]){[sender setTitle:@"A very long title for this button"forState:UIControlStateNormal];}else{[sender setTitle:@"X"forState:UIControlStateNormal];}}

This toggles between a long title and a short title for the button that triggered the event. Connect this action method to both of the buttons in Interface Builder.Ctrl-dragfrom each button to the view controller and selectbuttonTapped:in the popup.

Run the app and tap the buttons to see how it behaves. Perform the test in both portrait and landscape orientations.

Regardless of which button has the long title and which has the short

title, the layout always satisfies the constraints you have given it:

The lower button is always center-aligned in the window, horizontally.

The lower button always sits 20 points from the bottom of the window.

The top button is always left-aligned with the lower button and 40 points above it.

That is the entire specification for your user interface.

For fun, remove the Leading Alignment constraint (select it in the outline pane and pressDeleteon your keyboard), then select both buttons in Interface Builder and from the Align menu pickRight Edges. Now run the app again and notice the differences.

Repeat, but now chooseAlign\Horizontal Centers. That will always center the top button with respect to the bottom button. Run the app and see how the buttons act when you tap them. (Remember, if you get a dashed orange box when you change the constraints, you can use theEditor\Resolve Auto Layout Issuesmenu to update the button frames accordingly.)

Fixing the width

The Pin menu has an option for Widths Equally. If you set this

constraint on two views, then Auto Layout will always make both views

equally wide, based on which one is the largest. Let’s play with that

for a minute.

Select both buttons and chooseEditor\Pin\Widths Equally. This adds a new constraint to both buttons:

You have seen this type of constraint before, in the first part of

this tutorial. It looks like the usual T-bar but in the middle it has a

circle with an equals sign.

Even though there are two T-bars, in the Document Outline this shows up as a single Equal Widths constraint:

Changing the label text on one button will now change the size of the

other one as well. Change the bottom button’s label to “X”, just to

make it really small. You will notice that the top button no longer fits

its text:

So how does Auto Layout know which button’s size to use for both of

them? If you pay close attention, you’ll see that the top button’s frame

is no longer correct:

Obviously this is not what you want, so select the top button and chooseSize to Fit Contentfrom theEditormenu (or press⌘ =). Now the text fits inside the button again – or rather, the button fits around the text – and due to the Equal Widths constraint the yellow button also resizes.

Run the app and tap the buttons. The buttons always have the same width, regardless of which one has the largest label:

Of course, when both labels are very short, both buttons will shrink

equally. After all, unless there is a constraint that prevents it,

buttons will size themselves to fit their content exactly, no more, no

less. What was that called again? Right, the intrinsic content size.

Intrinsic Content Size

Before Auto Layout, you always had to tell buttons and other controls how big they should be, either by setting theirframeorboundsproperties or by resizing them in Interface Builder. But it turns out that most controls are perfectly capable of determining how much space they need, based on their content.

A label knows how wide and tall it is because it knows the length of

the text that has been set on it, as well as the font size for that

text. Likewise for a button, which might combine the text with a

background image and some padding.

The same is true for segmented controls, progress bars, and most

other controls, although some may only have a predetermined height but

an unknown width.

This is known as theintrinsic content size, and it is an important concept in Auto Layout. You have already seen it in action with the buttons. Auto Layout asks your controls how big they need to be and lays out the screen based on that information.

Usually you want to use the intrinsic content size, but there are some cases where you maynotwant to do that. You can prevent this by setting an explicit Width or Height constraint on a control.

Imagine what happens when you set an image on aUIImageViewif that image is much larger than the screen. You usually want to give image views a fixed width and height and scale the content, unless you want the view to resize to the dimensions of the image.

So what happens when one of the buttons has a fixed Width constraint on it? Buttons calculate their own size, but you can override this by giving them a fixed width. Select the top button and choosePin\Widthfrom the menu. This adds a solid T-bar below the button:

Because this sort of constraint only applies to the button itself,

not to its superview, it is listed in the Document Outline below the

button object. In this case, you have fixed the button to a width of 46

points.

You cannot simply drag the button’s resize handles to make the button

wider. If you do, you’ll end up with a whole bunch of orange boxes.

Remember that Xcode 5 does not automatically update the constraints for

you (unlike Xcode 4). So if you make a change to the button’s frame,

it’s up to you to make the constraints match again. The alternative

approach is to simply change the constraint instead.

Select the Width constraint and go to theAttributes inspector. ChangeConstantto 80 to make the button wider:

Run the app and tap the buttons. What happens? The button text does

change, but it gets truncated because there is not enough room:

Because the top button has a fixed-width constraint and both buttons

are required to be the same size, they will never shrink or grow.

Note:You probably wouldn’t set a Width constraint on a button by design – it is best to let the button use its intrinsic size – but if you ever run into a layout problem where you expect your controls to change size and they don’t, then double check to make sure a fixed Width constraint didn’t sneak in there.

Play around with this stuff for a bit to get the hang of pinning and

aligning views. Get a feel for it, because not everything is immediately

obvious. Just remember that there must always be enough constraints so

that Auto Layout can determine the position and size for all views.

Gallery example

You should now have an idea of what constraints are and how you can

build up your layouts by forging relationships between the different

views. In the following sections, you will see how to use Auto Layout

and constraints to create layouts that meet real-world scenarios.

Let’s pretend you want to make an app that has a gallery of your

favorite programmers. It looks like this in portrait and landscape:

The screen is divided into four equal quarters. Each quarter has an image view and a label. How would you approach this?

Let’s start by setting up the basic app. Create a new iPhone project using theSingle View Applicationtemplate and name it “Gallery”.

OpenMain.storyboard. From the Object Library, drag a plainViewobject onto the canvas. Resize the view so that it is 160 by 284 points, and change its background color to be something other than white (for example, green):

Note:There are two main reasons why you would drop a plainUIViewonto a storyboard: a) You’re going to use it as a container for other views, which helps with organizing the content of your scenes; or b) It is a placeholder for a custom view or control, and you will also set its Class attribute to the name of your ownUIVieworUIControlsubclass.

Let’s give this view some constraints. You’ve already seen two ways to make constraints: theEditor\PinandAlignmenus, andCtrl-draggingbetween views. There is a third method that you’ll use here. At the bottom of the Interface Builder window is a row of buttons:

The four circled buttons are for Auto Layout. From left to right they are: Align, Pin, Resolve Auto Layout Issues, and Resizing Behavior. The first three perform the same functions as the corresponding items from theEditormenu. The Resizing Behavior button allows you to change what happens to the constraints when you resize views.

Select the green view and click thePinbutton. A popup appears that lets you add a variety of constraints:

TheSpacing to nearest neighborsection at the top is what you’ll use most often. Click the four T-bar thingies so they become solid red:

This will create four new constraints between the green view and its superview, one for each side of the view. The actual spacing values may be different for you, depending on where you placed the view. (You don’t have the change these values to match mine). ClickAdd 4 Constraintsto finish.

Your storyboard should now look something like this:

This view needs four constraints to keep it in place. Unlike a button or label, a plainUIViewdoes not have an intrinsic content size. There must always be enough constraints to determine the position and size of each view, so this view also needs constraints to tell it what size it needs to be.

You may wonder, where are these size constraints? In this case, the

size of the view is implied by the size of the superview. The

constraints in this layout are two Horizontal Spaces and two Vertical

Spaces, and these all have fixed lengths. You can see this in the

Document Outline:

The width of the green view is calculated by the formula “width of

superview minus (98 + 62)” and its height by the formula “height of

superview minus (65 + 199)”. The space constraints are fixed, so the

view has no choice but to resize. (Again, your values may be different

depending on where you put the view.)

When you rotate the app, the dimensions of the superview change from

320×568 to 568×320. Plug this new width and height into these formulas,

and you’ll get the new size of the green view (408×56).

You can see this for yourself when you run the app and flip to landscape, but you can also simulate it directly in Interface Builder. Open theAssistant editor(press the button in Xcode’s toolbar that looks like a butler/alien) and selectPreviewin the jump bar:

Click the arrow button at the bottom to change the orientation to

landscape. This gives you an instant preview of what the storyboard’s

layout will look like in landscape orientation. The green view has

resized in order to satisfy its Horizontal and Vertical Space

constraints.

You can leave this preview pane open as you design your UI and it

will update automatically. You can also use it to toggle between the 3.5

and 4-inch form factors.

Note:Maybe you wondered why the constraint at the top of the view didn’t go all the way up to the top of the screen:

Instead it stops at the status bar. But in iOS 7 the status bar is always drawn on top of the view controller — it is no longer a separate bar — so what gives? When you created the constraint it didn’t actually attach to the top of the screen but to an invisible line called theTop Layout Guide.

On a regular view controller this guide sits at 20 points from the

top of the screen, at least when the status bar is not hidden. In a

navigation controller it sits below the navigation bar. Because the

navigation bar has a different height in landscape, the Top Layout Guide

moves with the bar when the device is rotated. That makes it easy to

place views relative to the navigation bar. There is also a Bottom

Layout Guide that is used for the tab bar and toolbars.

You may not always want yourUIViewto resize when the device rotates, so you can use constraints to give the view a fixed width and/or height. Let’s do that now. Select the green view and click thePinbutton; in the popup put checkmarks in front ofWidthandHeight.

ClickAdd 2 Constraintsto finish. You have now added two new constraints to the view, a 160 point Width constraint and a 284 point Height constraint:

Because Width and Height apply to just this view, they are located in

the Document Outline under the View itself. Usually, constraints

express a relationship between two different views – for example, the

Horizontal and Vertical Space constraints are between the green view and

its superview – but you can consider the Width and Height constraints

to be a relationship between the green view and itself.

Run the app. Yup, looks good in portrait. Now flip over to landscape.

Whoops! Not only does it not look like you wanted – the view has

changed size again – but the Xcode debug pane has dumped a nasty error

message:

Gallery[39367:a0b]Unable to simultaneously satisfy constraints.Probably at least one of the constraintsinthe following list is one you don't want. Try this: (1) look at each constraint and try to figure out which you don't expect;(2)findthe code that added the unwanted constraint or constraints and fix it.(Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentationforthe UIView property translatesAutoresizingMaskIntoConstraints)("","","","<_UILayoutSupportConstraint:0xc15dbd0 V:[_UILayoutGuide:0xc1a2d20(20)]>","<_UILayoutSupportConstraint:0xc1a1510 V:|-(0)-[_UILayoutGuide:0xc1a2d20]  (Names: '|':UIView:0xc1a2930 )>","<_UILayoutSupportConstraint:0xc1a3720 V:[_UILayoutGuide:0xc1a3230(0)]>","<_UILayoutSupportConstraint:0xc1a30e0 _UILayoutGuide:0xc1a3230.bottom == UIView:0xc1a2930.bottom>","")Will attempt to recover by breaking constraintBreak on objc_exception_throw to catch thisinthe debugger.The methodsinthe UIConstraintBasedLayoutDebugging category on UIView listedinmay also be helpful. . . .

Remember when I said that there must be enough constraints so that Auto Layout can calculate the positions and sizes of all the views? Well, this is an example where there aretoo manyconstraints. Whenever you get the error “Unable to simultaneously satisfy constraints”, it means that your constraints are conflicting somewhere.

Let’s look at those constraints again:

There are six constraints set on the green view, the four Spacing

constraints you saw earlier (1-4) and the new Width and Height

constraints that you have just set on it (5 and 6). So where is the

conflict?

In portrait mode there shouldn’t be a problem because the math adds

up. The width of the superview is 320 points. If you add the lengths of

the Horizontal Space and Width constraints, then you should also end up

at 320. The way I have positioned the view, that is: 98 + 160 + 62 = 320

indeed. Likewise, the vertical constraints should add up to 568.

But when you rotate the device to landscape, the window (and

therefore the superview) is 568 points wide. That means 98 + 160 + 62 + ?

= 568. There are 248 extra points that need to go somewhere in that

equation and Auto Layout doesn’t know where to get them from. Likewise

for the vertical axis.

The conflict here is that either the width of the view is fixed and

one of the margins must be flexible, or the margins are fixed and the

width must be flexible. You can’t have both. So one of these constraints

has to go. In the above example, you want the view to have the same

width in both portrait and landscape, so the trailing Horizontal Space

has got to go.

Remove theHorizontal Spaceat the right and theVertical Spaceat the bottom. The storyboard should look like this:

Now the view has just the right number of constraints to determine

its size and position — no more, no less. Run the app and verify that

the error message is gone and that the view stays the same size after

rotating.

Note:Even though Interface Builder does its best to warn you about invalid layouts, it cannot perform miracles. It will warn you when there are too few constraints but it doesn’t fare so well at detecting layouts with too many constraints. At least Auto Layout spits out a detailed error message when something is wrong. You will learn more about analyzing these error messages and diagnosing layout problems in “Intermediate Auto Layout” iniOS 6 by Tutorials.

Painting the portraits

Drag aLabelonto the green view. Notice that now the guides appear within that green view, because it will be the superview for the label.

Position the label against the bottom margin, horizontally centered against the guides. Add a space constraint to anchor the label against the bottom of the green view, at 20 points distance. The quickest way is to use thePinbutton and just select the T-bar at the bottom:

Now add a constraint to center the label horizontally. You’ve seen how to do this with theEditor\Alignmenu but you can also use theAlignbutton from the floating Auto Layout menu. Select the label and click the Align button to bring up the popup:

iOS 7:Auto Layout Tutorial in (Part 2)_第1张图片

Put a checkbox in front ofHorizontal Center in Containerand then clickAdd 1 Constraint. The storyboard should now look like this:

Notice that these two new Horizontal and Vertical Space constraints

are listed under the green view’s own Constraints section, not in the

main view.

Drag a newImage Viewobject onto the storyboard, and make the layout look like this:

The image view is pinned to the top, left, and right edges of its

superview, but its bottom is connected to the top of the label with a

standard spacing of 8 points. If you’re unsure of how to do this, then

follow these steps.

1. Drag the image view into the green view but don’t worry too much about its size or position:

2. With the image view selected, press thePinbutton and choose the following options:

The top, left, and right T-bars are set to 20 points but the bottom one is set to 8 points. Important: ForUpdate Framesyou should chooseItems of New Constraints. If you had left this to the default of None, the storyboard would look something like this:

The constraints you chose result in a different frame than the image view’s current position and size. But if you chooseItems of New Constraints, Interface Builder will automatically adjust the frame as it adds the constraints and everything looks dandy:

iOS 7:Auto Layout Tutorial in (Part 2)_第2张图片

Of course, if you do end up with a misplaced frame, you can use theResolve Auto Layout Issuesbutton to fix it:

Download theresources for this tutorialand unzip the file. You will find anImagesfolder – add this folder into your project. SetRay.pngas the image for the image view, change the image view’s mode toAspect Fitand set its background color to white. Change the label’s text to say “Ray”.

Your layout should now look like this:

Notice that the constraints inside the green view turned to orange.

This happened the moment you set the image on the image view. How come

your layout is suddenly invalid? Fortunately you can take the guesswork

out of it and let Xcode tell you exactly what’s wrong.

Click the small red arrow next toView Controller Scenein the Document Outline to view the issues:

iOS 7:Auto Layout Tutorial in (Part 2)_第3张图片

You have a Content Priority Ambiguity error. That’s quite the

mouthful. This is what it means: If neither the image view nor the label

has a fixed height, then Auto Layout doesn’t know by how much to scale

each if the height of the green view should change. (Interface Builder

seems to ignore for now that the green view actually has a fixed Height

constraint set on it.)

Let’s say at some point in your app the green view becomes 100 points

taller. How should Auto Layout distribute these new 100 points among

the label and the image view? Does the image view become 100 points

taller while the label stays the same size? Or does the label become

taller while the image view stays the same? Do they both get 50 points

extra, or is it split 25/75, 40/60, or in some other possible

combination?

If you don’t solve this problem somehow then Auto Layout is going to have to guess and the results may be unpredictable.

The proper solution is to change the “Content Compression Resistance Priority” of the label. You will learn more about that later on. For now, go into theSize inspectorfor the label and set the verticalContent Compression Resistance Priorityto 751. That makes it one higher than the priority of the image view. While you’re at it, setContent Hugging Priorityto 252.

The T-bars should turn blue again and the Auto Layout warnings are gone.

Adding the other heads

Drag the green view into the main view’s top-left corner. Recall that

the green view had Horizontal Space and Vertical Space constraints that

determined its position in the parent view. It still has those and they

cause the frame of the view to be misaligned.

To fix this, use theResolve Auto Layout Issuesbutton and chooseUpdate Constraints. Previously you usedUpdate Frames, which moved and resized the view the match the constraints. Here you want to do the opposite: you want the constraints to update to match the frame.

Note that the Vertical Space at the top is now negative. That happens

because this constraint is connected to the Top Layout Guide. But

there’s no reason why constraints cannot have negative values, so you

can leave this as is. (If it bothers you, delete that “Vertical Space

(-20)” constraint and pin the view to the top of the window.)

The Horizontal Space now has size 0 and is represented by a thick

blue line at the left edge of the window. So even though the view sits

completely in the corner, it still needs constraints to anchor it there:

iOS 7:Auto Layout Tutorial in (Part 2)_第4张图片

Select the green view and tap⌘Dto duplicate it. Move the duplicate into the top-right corner:

Notice that the T-bars are orange. When you made the duplicate, it

apparently lost its constraints for the X and Y position. To fix that,

pin the view to the top and the right edges of the window.

Duplicate two more times and put these copies in the bottom-left and

bottom-right corners, respectively. Again, pin these views to their

corners.

Change the screen design to the following:

Those are some good-looking programmers! :-)

Run the app. It looks good in portrait, but not so much in landscape:

It should be pretty obvious what went wrong: you’ve set a fixed width

and height on the four brightly-colored container views, so they will

always have those sizes, regardless of the size of their superview.

Select theWidth (160)andHeight (284)constraints from all four views and delete them (this is easiest in the Document Outline). If you run the app now, you’ll get something like this:

iOS 7:Auto Layout Tutorial in (Part 2)_第5张图片

Note:If you’re wondering why some of the views are larger than others, this is again related to the intrinsic content size. The size of the image determines how large the image view is; the size of the text determines how large the label is. Taken together with the constraints for the margins — 20 points on all sides — this determines the total size of each view.

This looks very much like the problem you solved in the introduction

in part 1, so if you think back to how you solved that, you’ll recall

that you gave the views equal widths and heights.

Select all four colored views. This is easiest in the Document Outline; holdand click on the four views. You can add the constraints in one go. In thePinpopup put checkmarks in front ofEqual WidthsandEqual Heightsand then pressAdd 6 Constraints.

Run the app again and rotate the device. Hmm… still no good:

All the viewsdohave the same height, and they also appear to have the same width, so your constraints are being met. It’s just not the width and height that you want them to have.

Just saying that all four views must have equal sizes is not enough

to determine what those sizes should actually be, because Auto Layout

does not know how these four views are connected to each other. They

appear side-by-side in the design, but there are no actual constraints

between them. Auto Layout does not know that it needs to split the

window width between the “Ray” and “Matthijs” boxes.

If Auto Layout can’t figure this out by itself, you have to tell it.

Select the Ray and Matthijs boxes and choosePin\Horizontal Spacingfrom theEditormenu. Because the boxes are side-by-side, this adds a Horizontal Space constraint with size 0 between them, and that is enough to let Auto Layout know how these two views are related. Also put a Vertical Space between the Ray and Dennis Ritchie boxes usingEditor\Pin\Vertical Spacing.

Run the app again, and this time it looks all right:

Note:Interface Builder still complains about misplaced views at this point. I’m not sure why that happens but it appears to be a bug in Xcode. If these warnings bother you, then select the main view (or the view controller) and from theResolve Auto Layout Issuesmenu chooseUpdate All Frames in View Controller. It does not change how the app works at runtime but at least it makes Xcode happy.

A quick note on the image views: they stretch out because you have not given them a fixed size. You may not know it, but that’s intentional on your part.

The image views wouldn’t fit in landscape mode otherwise. However, if you want an image view to keep its original aspect ratio, then you’re out of luck. You cannot achieve the following effect using Interface Builder:

iOS 7:Auto Layout Tutorial in (Part 2)_第6张图片

Unfortunately, Interface Builder does not currently provide a way to make constraints that keep the aspect ratio of a view intact. To do that, you need to create and set the constraints programmatically. You will learn how to do that in “Intermediate Auto Layout” iniOS 6 by Tutorials.

Where To Go From Here?

If you’ve made it this far, congratulations – you now know what Auto

Layout is all about, and have experimented with the basics! But there’s a

lot left to learn…

The tutorial you have just read is only the first half of the Beginning Auto Layout chapter from the bookiOS 6 by Tutorials. The second half teaches how to use Auto Layout to create more “real-world” screen layouts, and everything else you need to know about using Auto Layout from Interface Builder.

But like any visual design tool, Interface Builder has its limitations and sometimes it just makes more sense to work with theNSLayoutConstraintobjects directly from code. iOS 6 by Tutorials dedicates an entire chapter to this topic, Intermediate Auto Layout. So if you want to get to the bottom of Auto Layout,get the book!

你可能感兴趣的:(iOS 7:Auto Layout Tutorial in (Part 2))