Developer Tools • iOS, macOS, tvOS • 38:08
Auto Layout enables you to easily create robust layouts for your apps. Hear details about new and existing techniques for building layouts in Interface Builder. Learn how to build more dynamic layouts and handle state changes using priorities and size classes. Find out how iOS 11 support for dynamic type and safe areas can ensure your content can be viewed by anyone.
Speakers: Jonathon Mah, Jason Yao
Unlisted on Apple Developer site
Downloads from Apple
Transcript
This transcript has potential transcription errors. We are working on an improved version.
Good morning and welcome to Auto Layout techniques in Interface Builder. I'm Jonathon Mah. I'm an engineer on the Interface Builder team, and I'll be joined onstage shortly by my colleague Jason Yao. In this session, we'll be starting with a partly built iOS app and use that to demonstrate a series of layout and interaction techniques. Now it seems like every few months there's a new app to help people find dates, and this is another one of those months. So this is the app we're starting with.
[ Laughter ]
[ Applause ]
Like other dating apps using Auto Layout is about building relationships, relationships between views. And building relationships does take work. But it's the kind of work that's rewarding because having laid out our interface using constraints, our app can scale across a variety of screen sizes and adaptations, including ones which might not exist yet.
Now, if you've never used Auto Layout before, there's some great resources online and past sessions from WWDCs. But today we've got a lot of content to get through, so we're not going to cover the basics. In fact, we have six techniques that we'll be walking through. You can use these in your own apps, either as is or combine them together to form a larger effect. And with that, let's just jump right into the first one, which is changing layout at runtime.
Now at the top of our app we have a slider area where the user can set a distance. And usually, we expect the user to set this once and then not need to change it for a while. So we'd like for this area to be able to collapse away and reduce clutter in our interface.
Now, if we think about this from a layout perspective, what we're trying to do is collapse the height of this area to 0. So, if we add a height constraint and then set the constant to 0. If we did this in Interface Builder, we'd get something like this.
So what we're seeing here is our layout with a bunch of red lines indicating that our layout doesn't have a solution. We have what's called a conflict. So a conflict means that we've asked the layout engine to do multiple incompatible things. In this case, set the height of this area to 0 while also keeping the height large enough to fit the content inside of it.
The technique that we'll use to solve this is to take the view that contains our control, our slider, and our labels, and we'll wrap it in another view, here indicated in orange. Now instead of just collapsing the view containing our controls, we'll collapse this outer view. The inner view maintains its full height and satisfies all its constraints. But the outer view clips the inner view because clips ToBounds is enabled. And this gets the effect of hiding the interface elements.
Let's see how this looks in Xcode. So we have the slider area designed in Interface Builder right here, and we're designing it in the expanded state, so we can work with it more easily. So since it's expanded in Interface Builder, we'll be collapsing it at runtime. So we'll create a constraint at runtime to set this, the outer wrapping view height to 0.
And then, at some point, in response to the user touching the button, we'll activate that constraint. Now if that were the only thing we did we'd still hit the conflict situation that we saw before, so we'll deactivate a different constraint, the bottom edge of the control container to the wrapping view.
So this will allow the outer view to collapse. Let's look at the full code for this. This is in our view controller subclass and so we have three properties. Two of them are outlets to both that wrapping view and the lower constraint, and we have a property to store our 0 height constraint. In response to touching the button, the first thing we'll do is ensure that the 0 height constraint is created, if we haven't made it already. And then because we want the behavior to toggle between shown and hidden, we'll figure out which direction we're going.
With that done, we'll then deactivate and activate the constraint that we want to have -- that we want to get our interface into the final state. Now, if you notice carefully, we're deactivating one constraint before activating another constraint. We're not simply toggling both. And the reason for that is that Auto Layout tries to help us avoid the conflict situation.
Because, as soon as we get into a conflict, it'll log a message to the console saying hey, I've detected that these constraints are conflicting with each other. So if we activated, for example, 0 height while the bottom constraint was still activated, we'd get that message. By deactivating first, we avoid that.
So this is what we need to get our interface in the final state, but we also would like to animate the results so that the user can see -- maintain some context in the application as the transition happens. So to do that we'll use a UIView animation block, and we'll wrap a call to layout if needed. So this evaluates the constraints, gets frame values and applies to them to a view, and then UIView animation will capture that and animate the result.
So we're deactivating the bottom constraint and that gives us this animation effect. This isn't quite the effect that I want, but I can change that simply by keeping the code exactly the same and changing which constraint our edge constraint outlet is connected to. If I had said connected to the top constraint, then the animation changes and it gets the effect that actually I think I prefer.
So we just saw an example of changing the layout in our app by changing the constraints. Now let's look at another way to change the layout, and we'll use that to track touch. So we have this central card area in our app, and we'd like it to follow a touch around the screen and add a little rotation as you get close to the edge and snap back once you let it go.
So the view's position onscreen is generally defined by its frame and when we're using constraints and have constraints to this view, the layout engine owns the value of this frame. So the value itself is computed from the constraints. But there's another property that affects the view's position onscreen and that is the transform, and this applies an offset from the frame.
So the type of this is a CGAffineTransform. And this lets us add a translation, a rotation, and a scale that gets applied after the frame has been computed from constraints. So we can use this to get the effect that we're after, and I'll jump over to a demo and show you how it's done.
Okay. So here is our app in Interface Builder. And we'd like to make the central card area move around with the touch. Now I've just added a gesture recognizer, a pan gesture recognizer, from the object library to this area. And when I add a gesture recognizer, it appears at the top level of the scene, so [inaudible] and in the outline view. I'll open the assistant editor, so we can see our view controller code.
And I'll control drag from that pan gesture recognizer to a spot in our source code. And this will let me add an outlet. Or, in this case, I want to connect an action, and I'll call it panCard. I'll switch the type from any to pan gesture recognizer. This is the type of the parameter. And specifying pan gesture recognizer let's me use properties of that gesture recognizer within the action method. All right. So we have our method stub here. And I also have an outlet that's connected to that card view, which is what we want to move.
So, in this method, I'm first going to ask the sender, which is the gesture recognizer, for its translation. In effect, how far the user has panned or dragged. And then I'm going to use the result to apply a transform to this card view. Now the transform's computed by this helper method right here, which generates both a translation and adds a little rotation, depending on the exposition of the card. So I'm going to hit build and run, and we'll see how this looks.
All right. And so we have our app. And now, if I touch and move, we get a rotation, and it will follow the touch. And then if I let go, now the card just stays where it is, because we're never resetting the transform. It's just staying the last value that we gave it. If I touch and move again, we'll see that it will snap back to the original position. That's because the transform isn't offset from that. So, as soon as we go back with a new translation, we're going to be relative to the original frame.
So we'd like the card to return back to the center once we're done with it. Once we let go of the touch. Now we can do that by looking at the state of the gesture recognizer. So we have the same code here. And then, when the state is ended, we'll reset the transform back to the identity transform. Let's build and run again.
So now when we release the card snaps back. So, of course, we'd like to animate that snapping back, so let's just do that. So we have the same reset to identity code. And I'm using the UIView animate variant with the spring options. So this will give us a different effect. So I'll build and run again. So now, if I release the card, it will bounce back to the original position, and I kind of like this playful behavior.
[ Applause ]
Thanks. Yeah, I think people are going to show their friends this. So I think we're in a good spot. All right. So we just saw that the view position is a combination of multiple properties. Not only the frame, which is derived from the constraints, but we can also use the transform to affect where the view is onscreen. And that was just a couple lines of code to add this fun interaction.
So I hope that you guys will play around with that in your own apps, because it's a lot of fun. So transform is great for temporary changes, like we saw. Like tracking a touch or an animation between states in your interface. And then we reset back to the identity transform when we're done.
So now that we have our app acting playfully, let's talk about making the text look great. To do that, we'll support dynamic type. Dynamic type is a technology on iOS that provides both a set of text styles. For example, headline, body, caption. And user control over the size of those styles.
Here's a few example apps, and this is with a default text size set. Now, if the user preferred a larger text size, for example, we'd see messages adjust. So the text increases, and then all the graphic around -- the bubbles around the messages increase. Also the input field gets larger. Similarly, with calendar, an event layer. And our app looks like this.
Yeah, nothing changed. Or, depending on how we created our app, it might look like this. Where some text increases but our layout is not right, and its unreadable. Fortunately, Interface Builder makes it easy to fix this up for us. So let's take a look at how to do that right now.
All right. Back to our app, and let's take a look at making these two labels respect the system font size. Some I'm going to select this lower label here. And over in the inspector, I'll check the automatically adjust font checkbox. Now, if you've got a keen eye, you'll notice as soon as I did that, I had this little warning icon pop up. And this reminds us that for this property to take effect, we need to specify a text style for the label.
So right now the font for this label is a fixed value of system 12. And I'll change it to a text style by opening the inspector and selecting an appropriate style. In this case, I'll choose caption one. So this happened to correspond to system 12 at the default text style, so our canvas looks the same. I'll do the same with the upper label. This already had a text style set. And I'll build and run again.
Okay. So this comes up and it looks the same because we haven't changed the text size yet. Now we can change the text size by using the settings app on the simulator, but it's a little cumbersome to switch back and forth between settings and our app. So I want to show you another way to do that. If I jump back to Xcode, I can use the application menu to open develop a tool accessibility inspector.
So, from here, I can choose to target the simulator. And on the settings tab I get this font size slider. So I'll pull up the simulator side by side, and now I can change this slider and see our app react in real time. This makes it a lot easier to test dynamic type changes.
[ Applause ]
By the way, if I had an iPhone plugged in or set up for development here, I can also choose to target that connected phone. So we see our app adjusting -- reacting to the font size, but if it gets too large then our labels are running into each other and overlapping. So let's go fix that.
So, in Interface Builder, whoops, I'm going to jump back here. I want to get that out the way. The text area, down here, the size of that is defined by a fixed height constraint [inaudible] constant over here. Now, instead, we'd like this size to vary depending on the size of the labels within it. So we have the bottom label constrained to the bottom and the top constrained to the top. So, if we added a vertical constraint between the two, there'd be enough information to size this area from the constraints of the labels and the size of the labels themselves.
So let's do that. I'll control drag from label to another. And I can choose to add a vertical spacing constraint, so this will keep the labels separated by the same distance they are in the canvas right now. I can choose to add a vertical baseline standard spacing. So this is similar to vertical spacing except UIKit will adjust the constant of this constraint, depending on the text style and the current font sizes of the labels involved. So I'll choose to add one of those. And now I can select and remove the fixed type constraint and then build and run again.
So now things are looking a lot better. Our content area is adjusting for larger sizes and also shrinking down for smaller sizes, if people want larger information density on their displays. So, using two properties in the label inspector, both the font and the dynamic type property, we can make our labels adjust dynamically as the font size changes. And we saw using accessibility inspector to test different sizes.
There's a talk later this afternoon, Building Apps With Dynamic Type, with a lot more information, and after this session, if you want to try out how your own apps looks, if you change the font size, you can also get to the setting directly on your phone. If you go through accessibility, you get the larger accessibility sizes' option, which gives a large range of values for the type to vary on. So check that out. Now it's time to add some more views to our app. And I'd like to bring up Jason to introduce you to this safe area. Jason.
[ Applause ]
Hello, everyone. My name's Jason Yao. I'm an Interface Builder engineer, and it's a pleasure to be with you here today. We're going to continue on with our dating app journey and talk about three more techniques. And so, the first thing I want to talk to you about is what do you do when you got a view controller and you've got a navigation bar and a tab bar and you want to make sure your views are positioned so that they're not covered up by those bars? As you may have heard, there's a new layout guide on iOS 11 called the Safe Area Layout Guide. It is a new property on UIView. It works with Auto Layout and what it is is a rectangle that is around the bars and container edges and then you can constrain your views as you expect.
If you had to do this thing in the past, you may remember the Top and Bottom Layer Guide on UIViewController. Those are deprecated in favor of this new safe area. The Safe Area Layout Guide is easier to use. It's more expressive and allows you to do a few new things, like be able to center vertically between your bars. And that way as the [inaudible] and the size of your height changes, say if you rotate in landscape, your layout will adjust accordingly.
The Safe Area Layout Guide is also something that can apply to tvOS. When you target your app and your content for tvOS, you're going to want to fill the big screen. The thing you have to consider is that you're going to be on a wide variety of displays from small ones to big ones, flat screens to projectors, and you need to worry, in video terms, of something called the overscan region.
On some displays, my title here can be cut off because it's too close to the top. And so you want to adjust your content so that it's within the safe area. And the Safe Area Layout Guide is going to represent this region in iOS 11 for your tvOS storyboards.
Just constrain your views to the content and set it into the safe area and then fill the rest of your view with the beautiful background imagery for your users. It's really easy to enable Safe Area Layout Guide in iOS 11 and Interface Builder. You can go into your storyboard, go to the file inspector, find that box that says use Safe Area Layout Guides and check it. What's going to happen is that the top level view of each of your scenes is going to have the new Safe Area Layout Guide, and then you constrain to it just like you would to other things inside the canvas.
The Safe Area Layout Guide is a new property on UIView for keeping your content unobscured and title safe, and, if you use it on iOS 11 with -- for iOS storyboards, will automatically upgrade your constraints for you when you check that box. Thank you [applause] and that is because the geometry between the old Top and Bottom Layout Guides will match the new Safe Area Layout Guide. And best of all, that also means when you use Safe Area Layout Guides on storyboards in Xcode 9 we'll make it sure it's backwards deployable to previous iOSs.
[ Applause ]
Now I want to talk about a layout technique for being able to position yourself relative to your superview. This is called proportional positioning. And the idea is say if I have a dates' card in my dating app and I want to be able to position it about 70% down from the superview. How do I do this? There are a few techniques available. The one I want to show you today is the idea of using a spacer view.
Dragging out a view, just a normal UIView from the object library. Constraining it but not rendering it. And allowing it to be the scaffolding for positioning your view in place. And this technique also applies to other type of layouts that are used in proportion. Here's another example, where I've got a landscape view, with a 1/5, 2/5, and 2/5 proportion. And then they're centered along those proportions. So let's go ahead and take a look at how to do this inside of Interface Builder.
So I've got a basic layout already here. And, in this case here, I want to be able to position my date card. And it's being pushed in by a form sheet with the modal segue. So we're going to zoom in a little closer over here, and I've got two elements that already have some initial constraints. I've got a label and I've got an image. And if we select them both, if you look really carefully, you'll notice that it's blue on the left and right edges but the top and bottom are red.
That means there's some work that we need to do. Whenever you see red in the Interface Builder canvas, it can be one or two things. Either you have too few constraints and you're ambiguous, or you have too many constraints and you're conflicting. I just set this up so I know I haven't positioned myself vertically in the superview. So let's go ahead and do that. And we'll do it by creating the spacer view. So I'm going to type UIView in the object library. Just drag out the view and position it.
We'll give it just roughly the place that I want to see it in the canvas. Next, let's do a few setup things. First I'm going to mark it as hidden. This is going to [inaudible] a layout in runtime but we don't want [inaudible] and that way you don't incur the cost of drawing. The other thing I want to do to recognize it is, in the object library, rename it to spacer 70%.
Now we need to add constraints. So I'm going to click on the pin menu. We're going to add a top constraint, a leading constraint, and then maybe give it a width so I can do constraint connections to it. Add that. And you can see that it's already moved horizontally.
But we still have the top and bottom edges that are still red, and that's because we haven't specified a height yet. We want to make this proportion of the superview, so let's go ahead and do that. I'm going to add a equal height constraint by dragging out from the spacer to the superview and choosing equal heights.
Now it's using equal heights to my superview, but I realize I've got a navigation bar up there. And I actually wanted equal heights to -- well, actually, let's go ahead and set the proportion. And the thing is I'm still relative to my superview, and I want to be relative to my safe area.
And so normally you'd have to rebuild the constraint to do this. But I want to show you something new. If you go up into the attribute inspector, which is a really powerful thing, and you click on the item, we list now the neighbors that you can retarget your constraint items --
[ Applause ]
And so if I hit safe area, you can see it's now 70% to my safe area. Now let's go ahead and align our content. And so I'm going to take my dates' label, drag it over -- create a connection using control drag and align it to the bottom.
Now I wanted [inaudible] nicely along the baseline and so again, all I need to do is click on the constraint I just added. Go to the attributes inspector. And go ahead and set this to the other attribute. The really neat thing about Auto Layout is you don't have to do just bottom to bottom. You can do something like bottom to top.
Or, in this case, since I have a single line label, I'll do bottom to first baseline and that way, now we're 70% sitting right along the baseline. Let's test it out in a few configurations. So iPhone seven plus portrait using the device bar. And let's take a look in landscape and we're 70% there. And so this is what I want.
So when you need to do this proportional technique in Interface Builder, use spacer views to be able to get you there. And you be sure to mark those views as hidden so that they're not [inaudible] rendering but are assisting you at layout to be able to position your content. And if you're doing your layout programmatically, the thing you can use is the UILayout Guide. And so programmatically, you can use this as the equivalent to the spacer views.
Next. Let's go ahead and talk about another type of view that we want to layout. In this case, we're going to show you an example of adaptive layout in our dating app, where we've got this view of a 4 x 4 grid giving me a selection, and then it's got a label at the bottom.
But when I rotate my phone, it's actually going to give me something a little different. It's still going to give me the 4 x 4 grid of my dates, but it's going to have a text view over on the right. So let's go ahead and see how we could do adaptive layout using stack views in Interface Builder.
Okay. So I've got my dates storyboard over here and pretty much I've got this grid with an initial layout set up. I'm going to go ahead and first expand the object outline. I can hold down option and kind of expand everything all at once. And [inaudible] give you a tour of the setup.
We have a vertical stack view and it's tied with four constraints to the superview. And then it's got three rows, essentially. The first row is a nested horizontal stack view of the two images. Second row is also a nested stack view of the two images. And then the third row is a label. Now you can see that everything's kind of equally sized and that is because stack view has these great properties like alignment, distribution, and spacing. And there are a lot of options for being able to configure your layout using stack views and nests of stack views.
The great part about stack view is that it manages your constraints for you so there are very few constraints that you have to add. If you want to find out more about the other, distribution and alignments options, be sure to check out the documentation. Right now we're using fill equally to make sure everything's properly sized. And let's go ahead and tune this closer to what I want.
Okay. So let's first adjust for spacing. So I'm going to select all [inaudible] command and select all my stack views. And then we're going to give it a slightly larger spacing here in the attribute inspector. I'll set it to maybe 30. That's a little too big. So a new to Xcode 9 and iOS 11, we have a standard spacing option that allows the system to choose for you.
I'm going to use that. Now, if you remember back to Jonathon's talk about dynamic type, be sure to check out standard spacing for vertical stack views using content that's using text styles and checking this baseline relative. You'll get the same effect of dynamic type that you just saw.
For us, since we have images and labels, we're going to go ahead and just leave it unchecked. And the next thing I want to do is I want to really make sure these images are square. Now they may look square to you but they really aren't, and I'll prove it to you. I'm going to control drag from the image to itself and select the aspect ratio constraint. And you can see when I pop this open it actually says a multiplier of 193 to 226. Let's go ahead and make it 1:1.
Okay. But something didn't quite happen there. Nothing happened in canvas and, in fact, my aspect ratio is red. And what's worse, if you take a look over here, there's a little red dot. And so, when we click on that, we have a whole bunch of conflicting constraints. What happened? Well, let's go ahead and debug this together. And I'll give you -- we'll show you a technique for how to approach this type of problem within the canvas. So I've got the aspect ratio and I want to, first of all, validate that this is the thing that introduced error.
Some I'm going to go to the attribute inspector for the constraint and then toggle the installed button on and off to see what happens. And so, when I toggle it off, my issues disappear. And when I toggle it on, they appear again. But nothing's really happening in the canvas, so the next strategy I'd like to do is I like to poke around in my layout. And the thing I care about is probably the other red constraints that are nearby. Let's go ahead and take a look at the one from the outermost stack view to the top.
And when we uninstall that, the issues disappear but we're no longer fitting in the superview. But the images are nice and square. That's kind of an interesting effect. Let's go ahead and do another one. So I'm going to reinstall this one. And I'm going uninstall the trailing constraint.
And you can see again we're square but this time we're overflowing our superview. And so what's actually happening is that we can't satisfy both the aspect ratio constraint, the fill equally distribution on the stack views, and then have it fit into the superview. Luckily, for my case, I don't need to fill the entire superview. What I really want is actually to just stick to the top. So if I uninstall this, this is closer to what I want.
But supposing we rotate in a landscape and my height gets smaller, I still want this constraint so [inaudible] that either. And so what I'm going to do is I'm going to leave it installed and set it to greater than or equal to, and then this is more of what I want in portrait.
Let's go ahead back to the object outline and we're now going to look at landscape. When I click the device bar on landscape, you can see there's some work that we have to do here. The first thing we need to do is we want to get closer to the look that we were wanting. And, if you remember, the top [inaudible] label needs to disappear, and then we're going to eventually put a text view over on the right.
So let's first work on the top [inaudible] label. I'm going to click on it and a stack view -- the thing we could do is we could use the hidden property. Stack view is really great with hidden subviews because it will also collapse their layout but keep them in the arranged subviews array. Now, we'll go over to hidden property, and actually new to Xcode 9, we also allow you to vary this by size class. And so you can see --
[ Applause ]
There's a plus button to make that easier. And so I'm going to pop it up and choose any width. And in this case, because it's landscape, it is compact type. You could also verify that with the device bar on the bottom. Right now we're in landscape iPhone seven plus with regular width and compact type. So for any width compact type, I'm going to add a variation, mark it as hidden, and there we go. And, if we switch over to portrait, it appears.
Going back to landscape, it's disappeared. Okay. We need to add our text view now. And, in order to do that, we need a outer horizontal view. We need some sort of horizontal stack view for being able to host the text view. So I'm going to select everything here.
And then I'm going to type text view in the object outline because I want to add that in. Let's go ahead and embed the outer view using the embed and stack button. So it created a new stack view, and we need to switch it over to be -- that's vertical. We need to switch it over to be horizontal.
Everything shrunk down because also, since I embedded it into new view, it got rid of my constraints to the superview. So we need to make it horizontal. Let's go ahead and give it a standard spacing for the outermost one we just created. And then we can add a new text view. So we'll just go ahead and drag that in.
There it is. And then we need to rebuild the constraints to our superview for the outermost one. So I'm going to go back to the pinning menu 10, 10, 10, 10. And then the bottommost constraint, if you remember -- I'll zoom in here -- is going to be greater than or equal to.
Okay. And so this is what I want for landscape. When we switch back to portrait, we actually have to do a similar thing and hide the text view by a size class. And so we can click on this. Go over to the hidden menu again and add a variation -- Of any width, regular height, and then mark it as hidden. And now we have the look that we want.
[ Applause ]
So use the stack view, when you can, for all your layouts. Alignment, distribution, spacing. Those are properties that will get you there, including nesting. Add just a few constraints. We only noticed it needed aspect ratio constraint to get the proportion. So you only need a few constraints to get you a little bit extra if you have some requirements like the proportion. The hidden property in Xcode 9 is size classable, which is great. You can go ahead and use it, and it works great with stack view, because stack view will collapse hidden subviews. And best of all, varying by size class with the hidden property is backwards deployable.
[ Applause ]
So, in summary, we've looked at six techniques today, and this is going to add to your arsenal for being able to build out your apps. We cannot wait to see what you build. And today we looked at some techniques for being able to do dynamic layout within the runtime.
Things like dynamic type, as well as layout techniques, for being able to handle and make your content look great, unobscured, and handle adaptive layout. For more information, you could check out the developerapple.com. If you want to learn more about dynamic type, you could check out the session this afternoon and there are also links to past videos. Have a wonderful WWDC, everyone.
[ Applause ]