From the iPhone 4S to the iPad Pro, vastly different device resolutions are available that your app needs to look good in. After adding landscape and portrait to the mix, plus all the different multitasking windows that your app can find itself in, it’s a headache to think about designing different fixed app layouts for all the different combinations and permutations.
There must be an easier way. How can the interface of an app look great regardless of its environment?
Over the years, Apple has introduced several different approaches for setting up a layout that adapts to its environment. In this chapter, we’ll look at various solutions and how to choose between them.
Before we look at the solutions, let’s look closer at the problems we’re facing.
Once upon a time, there was one iPhone, and everyone was happy. Pixels were the same as points, and developers knew the resolution of the screen they were developing for. Fast-forward to today, and there are multiple devices on the market, let alone in people’s hands, and multiple point resolutions that an app needs to look great on (see figure 6.1).
Apps don’t display only in portrait orientation. Apple recommends that apps display in both portrait and landscape orientation, where possible. Oops, that doubles the number of resolutions your app layout must accommodate (see figure 6.2).
In iOS 9, Apple introduced different multitasking capabilities to iPads. Slide Over, available on most iPads, allows you to drag in a narrow version of an app. Split View, available on newer iPads, allows two apps to run side by side, at a width customizable by the user, giving iPads nearly infinite combinations of widths.
View controller root views don’t always fill the screen. View controller views can take up a portion of a screen, for example, when presented as a popover or as part of a split view controller.
These factors—device resolutions, device orientations, app window sizes, and different view controller sizes—need to be considered when presenting a scene in your app.
And it doesn’t stop there. Up to now, we’ve looked at how the layout of a scene’s content must adapt to its environment. What if the content itself changes? A label could display dynamic text, for example, or your app could support different languages.
Whether the pressure is from external or internal forces, the layout of your app needs to adapt. But how?
Auto layout is a technique for describing interfaces using constraints. The Auto Layout engine uses these constraints to calculate how to lay out your app’s interface.
In this book, you’ll build a Bookcase app in which a user can keep track of books in their bookcase. The designer has sent through the interface in figure 6.3 for adding the details of a book. You’re going to lay out this scene, exploring auto layout!
Open the repo here, which contains the interface ready to lay out with auto layout from https://github.com/iOSAppDevelopmentwithSwiftinAction/Bookcase.git (Chapter6.1.InitialSetup).
In auto layout, views have a number of main layout constraints. Constraints are divided into size and location, which is divided further into horizontal (x-axis) and vertical (y-axis). See table 6.1.
Categories |
Attribute |
Additional information |
---|---|---|
Location: Horizontal | Leading, Trailing | Constraints on the left (leading) and right (trailing) of a view. In right-to-left languages, their directions swap. |
Left, Right | It’s usually preferable to use leading and trailing. See the note following the table for more information. | |
Center X | ||
Location: Vertical | Top, Bottom | |
Center Y | ||
Baseline, First Baseline | Text views such as labels contain a baseline representing the bottom of the first or last lines of text (excluding descenders that drop below the line in letters such as j, p, or q). | |
Size | Width, Height | Can be an absolute value, or relative to another view’s size constraint. |
Aspect ratio (based on width and height) | To constrain a view’s aspect ratio, constrain its width to its height. |
You may wonder, if leading and trailing are constraints on the left and right, wouldn’t left and right attributes be redundant? Well, there’s a difference. Although leading is on the left and trailing on the right if the device’s current language is left-to-right (such as English), the two attributes switch sides if the current language is right-to-left (such as Arabic). Why would they do this? Whereas left-to-right language speakers expect to see their most important content on the left, right-to-left language speakers expect it on the right. It’s recommended to use leading and trailing constraints over left and right, because they ensure the most important content is always in its correct place.
See figure 6.4 to help visualize these attributes.
Size constraints are the only constraints that can be a value in themselves, without relating to another view. All location constraints must (and size constraints can) specify the view that they relate to. It can make sense to say view A is 50 points high, but it doesn’t make sense to say it’s 50 points in the y direction ... away from what? Fifty points away from its superview top? Fifty points away from another view’s bottom?
Auto layout can be a complicated topic at first, but with practice, the process of describing your layout using constraints will become easier.
After laying out the interface in the storyboard, it can help—especially while learning auto layout—to sketch out the constraints on paper, separating the horizontal and vertical constraints. Then ask yourself three questions:
Let’s use auto layout in Interface Builder to describe the add-a-book form interface.
Open the main storyboard and select View As at the bottom left of the storyboard canvas. This opens the device configuration bar, where you can select to view the storyboard from the perspective of different devices or orientations (see figure 6.5).
You’ll find the interface has been laid out nicely in the storyboard for when you’re viewing it as iPhone 8, but because no adaptive layout has yet been implemented, it looks wrong viewed on other devices or orientations. Because views are merely positioned from the top left by default with absolute width and height point values, in different resolutions they can appear positioned incorrectly, or cut off (see figure 6.6).
Let’s implement auto layout constraints to rectify the situation.
Luckily, our helpful designer has sketched out how they want the design to look, separating the horizontal constraints (width and location) from the vertical constraints (height and location) and adding helpful comments. Take a moment to familiarize yourself with the horizontal constraints in figure 6.7. Confirm for yourself whether you could determine the x position and width of every view in the design based on these constraints, regardless of the width of the root view. (Don’t worry if certain rules aren’t clear yet; we’ll look at each of them in turn.)
Before we look at the vertical constraints, how about adding a couple of horizontal constraints? Let’s start with the horizontal constraints on the book view.
If you select the book image and examine its attributes in the Attributes Inspector, you’ll find that it’s set to Aspect Fit mode. This means that it will fit the image inside its boundaries (represented in figure 6.7 by the shaded rectangle), but maintain its aspect ratio. This has been set to Aspect Fit mode, so that in the future, if this image is replaced by a wide and short image, it will fill out the available space. We’ll look more at image views in chapter 13.
Add your first constraint on the book image: 16 points from left safe area layout guide. Follow the steps in figure 6.8.
When you create your first constraint, red error lines will appear in the canvas. Auto layout errors can indicate one of two types of errors, as explained in table 6.2.
Error |
Description |
---|---|
Unsatisfiable layout | Two or more constraints are in conflict. |
Ambiguous layout | Your layout has two or more solutions, and the Auto Layout engine isn’t clear which is preferable. |
At this early stage, these error lines indicate that your layout is ambiguous because you have more constraints yet to define! You’ll also see an error arrow at the top right of the document outline. Select this arrow to get more information about the problem. You’ll find that the book view is missing a constraint for the y position. Not to worry, you’ll get to that when you look at the vertical constraints!
The root view of each scene automatically contains a safe area, bordered by what are called safe area layout guides. Pinning your view to safe area layout guides ensures that your view is not obscured by other interface elements such as status, navigation, and tab bars.
The book’s width is represented with a dotted line in figure 6.7 because it’s implicitly defined by other constraints. If the book view is pinned to the left and right of its superview, there’s no option but for the width to fill the available space. If you were to specify an absolute width for the book, the layout may work for one resolution, but if the superview had a width of any other value, the layout rules would cause an unsatisfiable layout.
Similarly, the widths of the title text field, author text field, and notes text view are implicitly defined by the width of views to their left and right. If the root view is displayed on a wide device or orientation, these three views will merely grow to fill the available space.
Add the right constraint for the book:
Certain types of views have an intrinsic content size. Labels and buttons, for example, have an intrinsic content size defined by their content, but text fields and switches have a default intrinsic content size. Image views, such as the book and the star-rating view in our example, have an intrinsic size defined by the size of the image.
If you don’t specify a size for a view with an intrinsic content size, the Auto Layout engine will assume the intrinsic content size to determine the size of the view.
Intrinsic content size is why you won’t need to specify height constraints for any of the labels or text fields in the example. Their intrinsic height works fine with the design.
Plain views don’t have an intrinsic content size, so you have to define their size in auto layout. If you have created a subclass of UIView, for example, you can set its intrinsic content size in the code by overriding the instrinsicContentSize property.
A height constraint will generate based on 100% of the root view in the canvas. That’s not exactly what you were after, so you’ll need to edit the additional constraint options.
Occasionally, you’ll want to make more-detailed edits to a constraint. You can make these edits in the constraint options.
Notice in the options that constraints also have priorities. Priorities range from 1 to 1,000, clustering around 250 (low), 500 (medium), 750 (high), and 1,000 (required). You can use priorities to describe your preferences and help the Auto Layout engine understand how to resolve ambiguities. We’ll come back to priorities shortly.
Now, to center the star-rating view. You could Control-drag from the star-rating view to its superview again (this time, dragging up). But for a change, you’ll use the Align button. In the bottom, right-hand corner of the canvas, you’ll find five curious buttons (see figure 6.12).
We’ll come to each in turn, but first let’s look at the Align menu: . When you have two or more views selected, you can use the Align menu with two views selected to align their edges, centers, or baselines. When you only have one view selected, you can use the Align menu to center a view horizontally or vertically in its container.
The star-rating view should now have an x position (center) and width and height (from intrinsic content size) and only needs a y position. The designer’s brief suggests it should be a standard distance from the book view. A standard distance lets the Auto Layout engine choose the most appropriate value.
Now, you’ll add a y position to the star-rating view, using the Add New Constraints menu: .
In this menu, you can also see which view Interface Builder has detected that you’re most likely intending to pin your view to. You can make a change to this here, if necessary, but your intention was to pin the star-rating view to the book view, Interface Builder has guessed correctly!
Practice using the Add New Constraints menu, by adding constraints for the notes text view. Pin it a standard distance on the left, 0 points from the bottom safe area layout guide and 16 points to the right safe area layout guide. (At the time of writing, standard distance isn’t available when pinning to the safe area layout guides.)
You can also use the Add New Constraints menu to pin multiple views simultaneously.
The title text field, author text field, and notes text view all need to be separated by a standard distance, pinned to the right safe area layout guide, and a standard distance from their associated labels.
Why wouldn’t you need to specify an absolute width for one of these three labels? You may have guessed it: the labels have an intrinsic content width based on their content. The only way for all three widths to be equal is for them to be equal to the widest option—otherwise, two labels would have to shrink smaller than their content width.
But as you’ve seen, text fields have an intrinsic content size too. If both labels and text fields have an intrinsic content size, how does the Auto Layout engine know which views to stretch and shrink to accommodate different resolutions?
Imagine a hypothetical interface laid out with one view containing two labels. If the view width increases, the first label should stay the same width and the second label should stretch to fill the available space. If the view width decreases, again the first label should stay the same width and the second label should shrink to its available space (see figure 6.17).
How would you indicate this preferred behavior to the Auto Layout engine? Your first thought might be to specify a fixed width for the first label, but what happens one day when you decide to localize your app into different languages, and the word for “title” in another language is shorter—or worse, longer? Do you manually update the width of the title label for every language you’ve localized your app into? Or do you change the width value to the longest possible version of the word “title”? A better approach is to allow the intrinsic content size of the label to do its job in defining the width of the label, and let the Auto Layout engine know which label you’d prefer to shrink and stretch if necessary. How? By setting the label’s Content Hugging Priorities and Content Compression Resistance Priorities.
The higher the priority of Content Hugging, the more a view tries to hug its intrinsic content width and resists stretching. The higher the priority of Content Compression Resistance, the more a view resists compression of its content, or shrinking. See figure 6.18 to help grasp these concepts.
The default Content Hugging Priority for a label is 251 (low) and the default Content Compression Resistance Priority is 750 (high).
In the hypothetical interface of figure 6.17, to indicate that your preference is for the second label to shrink or stretch if necessary, you could give the second label lower priorities for horizontal hugging and compression in the Size Inspector (see figure 6.19).
Conversely, you could also have given the first label higher horizontal hugging and compression priorities.
Great! But back to the add-a-book interface layout. Will you need to make changes to these priorities to indicate whether the title label and title text field should shrink or compress beyond their intrinsic content size? Well, no!
Because it’s a common preference in a layout for a text field to grow or shrink while the label remains the same width as its content, defaults to accommodate this are baked into the system. Although the label hugging priority default is 251, the text field hugging priority default is 250. The text field will be the preferable view to stretch rather than using a label. Because the text field’s intrinsic content width is a bare minimum width of 25 points, it’s fine for the text field to continue to resist compression at the same priority level as other views.
Now, all that’s left is to give these three labels a y location.
That’s it—you’ve fully described the add-a-book form using auto layout constraints. Well done!
If you select all the views now by clicking on the storyboard and selecting Command-A, you should (hopefully) no longer be seeing any red error lines or arrows. You may, however, still see orange warning lines, most likely indicating several views are slightly misplaced on the canvas. Select the orange warning arrow at the top right of the document outline and skim over the issues.
Sometimes, you might have issues with your auto layout constraints, but two buttons can help you out:
If you have any orange warning lines indicating a misplaced view, let’s resolve the issue now by updating frames.
If you don’t have any orange warning lines, but you’re interested in experimenting with updating frames, feel free to misplace a view by dragging it to where it shouldn’t be!
Regardless of the device you run your app on, the layout should adapt based on the rules you specified as constraints. See figure 6.20 for how your interface should look now on three different devices.
Congratulations, you’ve laid out an interface using constraints and auto layout!
Run the app yourself to confirm that your design adapts to different device resolutions. You can rotate the simulator by holding down Command and the left-arrow (←) or right-arrow (→) buttons, and the interface will adapt to its new environment. Your app will even adapt to multitasking modes such as Slide Over or Split View!
If you’d like to compare your project with mine at this point, you can check mine out here: https://github.com/iOSApp-DevelopmentwithSwiftinAction/Bookcase.git (Chapter6.2.AutoLayout).
If you’re using an iPhone device or simulator, you may have discovered that the simulator doesn’t want to rotate in one orientation. What’s going on?
Because iPhone users are less likely than iPad users to want to use their handsets upside down, the default approach is to indicate to the user that the handset is upside down by not triggering a rotation for this orientation. This is set in the project’s general target settings, under Deployment Info, where Upside Down Device Orientation is unchecked by default for Universal devices. (If you dig down and select iPad devices, you’ll find that iPads override this universal behavior by allowing upside down device orientation.)
For more on project settings, see appendix A.
Because Interface Builder gives you the capacity to visualize your layout, and immediate feedback on errors and warnings, it’s the best place to set up your constraints if possible. However, occasionally you may want your scene, or views within your scene, to change state, for example, after user interaction. This adjustment to your layout might need to be handled in code.
Luckily, it’s possible to work with auto layout constraints in code. There are three main approaches you can use that are syntactic differences that, in the end, produce the same result: constraints that the Auto Layout engine can use to lay out a scene. You can set up your constraints with combinations of different approaches.
The following is a brief overview of the three different approaches to defining your constraints programmatically. Whichever approach you use, you’ll generate an NSLayoutConstraint object. After generating the constraint, you then need to activate the constraint. (This is an easy step to forget.)
You can activate constraints in two ways:
constraint.active = true
NSLayoutConstraint.activate(constraints)
NSLayoutConstraint is a powerful but verbose approach to defining individual constraints.
Back in figure 6.11, you saw that setting up the options that define a constraint is like formulating an equation. Using NSLayoutConstraint, you can create a constraint by passing in all the components of that equation and then activating the constraints (see figure 6.21).
Visual Format Language (VFL) takes a different approach to defining constraints. In VFL, you can describe multiple constraints simultaneously.
Rather than setting up each individual constraint, you describe the horizontal and vertical sketches of your layout as strings in a visual format. Probably the easiest way to get grasp VFL is by looking at an example. Imagine you want to set up the horizontal constraints of a label and a text field, side by side, filling the available space.
First, set up a dictionary containing the elements:
let views = ["label": label, "textField": textField]
Then, you need to describe the horizontal layout, using VFL. See figure 6.22 for your first look at the VFL syntax.
In English, this string says, “In the horizontal direction, place the label a standard distance from the left edge, place the text field a standard distance away, and then place the text field a standard distance away from the right edge.”
This VFL string will automatically set up horizontal constraints for both the label and textField; not bad for a little string!
Once you define a VFL string, you can pass this and the dictionary of views into NSLayoutConstraint’s constraintsWithVisualFormat method, and then activate the constraints you generate, as shown in the following listing.
let views = ["label": label, "textField": textField] let formatString = "H:|-[label]-[textField]-|" let constraints = NSLayoutConstraint.constraints ( withVisualFormat: formatString, options: [], metrics: nil, views: views) NSLayoutConstraint.activate(constraints)
VFL does have a limitation—it doesn’t support multipliers. If multipliers are necessary, VFL needs to be used in combination with other techniques.
NSLayoutAnchor creates individual constraints in a way similar to NSLayoutConstraint, but with a more succinct syntax. Every view and layout guide has anchors representing the 12 main constraint types (listed in table 6.1). You can constrain these anchors directly to each other to generate NSLayoutConstraints. Again, it’s a matter of passing in the different components of the constraint equation, but in a different way (see figure 6.23).
If the defaults for multiplier (1) and constant (0) are sufficient, these parameters can be left out of the call.
You’ll practice using programmatic constraints by adding layout anchors to adapt views that were already created in code.
Open the ViewsInCode project from chapter 4, or if you prefer, you can download it from https://github.com/iOSAppDevelopment-withSwiftinAction/ViewsInCode.git (1.Displaying-Views branch).
Remind yourself of the project by running it on the simulator. In this project you added a red view that was the width and half the height of the root view. You then placed a label half-way down.
Great! These coordinates and dimensions were relative to the size of the root view, which was, by default, the same size as the app window. Regardless of whether you run this app on an iPhone 4S or on an iPad Pro, the calculation is correct.
But wait, there’s a problem. Rotate the simulator, and you’ll see something like figure 6.24.
When the app window rotates to another orientation, the scene’s root view automatically rotates, but any subviews don’t automatically resize or reposition to the root view’s new dimensions. What can we do about that?
Auto layout to the rescue!
let constraints:[NSLayoutConstraint] = [ //red view redView.topAnchor.constraint(equalTo: view.topAnchor), redView.leadingAnchor.constraint(equalTo: view.leadingAnchor), redView.trailingAnchor.constraint(equalTo: view.trailingAnchor), redView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.5), //label label.topAnchor.constraint(equalTo: redView.bottomAnchor, constant: 8), label.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor) ] NSLayoutConstraint.activate(constraints)
Each view has a translatesAutoresizingMaskIntoConstraints property, which, if true, will automatically convert your autoresizing masks into auto layout constraints. We’ll look at autoresizing masks in a moment, but for the moment it’s sufficient to understand that these additional, automatically generated constraints are conflicting with the constraints you’re manually creating.
The translatesAutoresizingMaskIntoConstraints property defaults to true, but will automatically swap to false if you add auto layout constraints to a view in Interface Builder. If you plan to add your views programmatically using auto layout as you’re doing now, you must set this property on each view in code to false, or these automatically generated constraints may conflict with yours.
redView.translatesAutoresizingMaskIntoConstraints = false
label.translatesAutoresizingMaskIntoConstraints = falseThat’s it! You’ve set up sufficient constraints for the red view and label to know how to display, regardless of the device or orientation they’re displayed in.
Before auto layout, Apple’s first attempt at solving the problem of adaptive layout was called autoresizing, known also as springs and struts. As you’ll see, autoresizing does have limitations compared to auto layout, but can still be a useful tool for quickly building simple interfaces, and the more powerful auto layout can be implemented for more-complex layouts.
As you’ve seen, views by default maintain the absolute position (x-y) and size (width-height) that they’re instantiated with. If a view’s superview (such as a scene’s root view) changes size (perhaps due to a rotation), its subviews by default won’t adjust accordingly. Autoresizing aims to correct this by adding certain rules that determine how a view resizes when its superview resizes.
The four outer margin attributes (top, left, right, and bottom) and the two size attributes (width and height) can be set to flexible (the springs) or fixed (the struts). See figure 6.25 to see how springs and struts define a view’s relationship with its superview.
We’re going to explore how autoresizing works in code by looking again at the ViewsInCode project.
You could remove (by selecting Source Control > Discard all Changes) or comment out any adjustments you made to the layout earlier, or if you prefer, you can check it out at https://github.com/iOSApp-DevelopmentwithSwiftinAction/ViewsInCode.git (1.DisplayingViews branch).
You can comment out code by surrounding it with forward slash + asterisk (/*), and asterisk + forward slash (*/); for example: /* Comment */.
Let’s consider how autoresizing could be used to automatically resize the red view and the label of the ViewsInCode project. Take another look at figure 6.24 to remind yourself of what the intention of the interface is, and then look at a written description of that intention for the red view in table 6.3.
Attribute |
Description |
---|---|
Left margin | 0 |
Width | The width of the superview |
Right margin | 0 |
Top margin | 0 |
Height | Half the height of the superview |
Bottom margin | Half the height of the superview |
Describing the interface this way helps make it clear which attributes are relative to the size of the superview (and therefore springs), and which are absolute values (and therefore struts).
When setting up the autoresizing rules for a view, rather than specifying all six attributes, only relative or flexible measurements (springs) are specified, and all unmentioned attributes are assumed to be absolute or inflexible (struts).
The width, height, and bottom margin are all relative to the superview, so you’ll set them to be springs. Use the UIView’s autoresizingMask property, and pass in an array containing the three flexible attributes.
redView.autoresizingMask = [.flexibleHeight, .flexibleWidth, .flexibleBottomMargin]Follow the same process for the label described in table 6.4.
Attribute |
Description |
---|---|
Left margin | 20 |
Width | Fixed width |
Right margin | Width of superview (minus) width (minus) left margin |
Top margin | Half the height of the superview |
Height | Fixed height |
Bottom margin | Superview height (minus) height (minus) top margin |
label.autoresizingMask = [.flexibleTopMargin, .flexibleBottomMargin, .flexibleRightMargin]
Close, but not perfect! The red view resized great, but the label is slightly off. The simple calculation that Xcode performs to determine the top and bottom margins of the label aren’t sufficient to place it precisely where you’d like it. This is a case where a more precise method such as auto layout will be necessary to perfect the layout.
You can perform autoresizing in Interface Builder as well. After laying out your views, you go to the Size Inspector for each view, where you’ll find an autoresizing section with the six autoresizing attributes that you saw in figure 6.5.
Click on the six attributes to turn them on and off. Margins attributes are represented by struts (lines with flat ends), and size attributes are represented by springs (lines with arrowheads). The default attributes are represented by the Top-Left margin struts turned on and the Left-Bottom struts and Width-Height springs turned off (see figure 6.27). Notice the image to the right of the autoresizing attributes gives you a visual indication of the expected result with this combination of springs and struts.
For example, to replicate the red view’s attributes from table 6.3, you’d turn on the Width-Height springs, and the right margin strut (see figure 6.28).
You probably already have noticed some limitations of autoresizing:
How does Xcode know to use autoresizing or auto layout to lay out your interface? When you drag a view onto Interface Builder, it will by default begin with top and left struts defined in autoresizing. As soon as a view contains at least one constraint, Interface Builder assumes you’re planning to use auto layout constraints on this view rather than autoresizing, and the autoresizing attributes disappear from the Size Inspector.
Using autoresizing in a scene doesn’t prevent you from using auto layout on other views in the same scene. It’s possible to lay out a scene using the simpler autoresizing and then incrementally adopt the more powerful and complex system of auto layout on views where it’s needed.
Auto layout (along with size classes discussed in the next chapter) will be sufficient for most interfaces, but occasionally you may need to implement adaption and transitions of your interface manually in code.
To explore manual adaptive layout, you’ll work on the same ViewsInCode project from the previous section, but from its initial state.
Again, discard or comment out changes, or check out the repo again from https://github.com/iOSAppDevelopmentwithSwift-inAction/ViewsInCode.git (1.DisplayingViews branch).
Whenever the root view of a view controller changes size (such as when the app rotates), a UIViewController event called viewWillTransition()triggers. This method passes in an argument containing the new size of the root view.
Override this method, and use this size parameter to resize the red view and reposition the label, as shown in the following listing.
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { 1 super.viewWillTransition(to: size, with: coordinator) 2 self.redView.frame.size = CGSize(width: size.width, 3 height: size.height / 2) 3 self.label.frame.origin.y = size.height / 2 4 }
Run the app again, and rotate the simulator to test your repositioning code. The end result of the rotations is great, but the rotation transition doesn’t look quite right and it’s hard to tell exactly what’s happening with the speed of the transition. Slow down the transition to get a better look at it by selecting Debug > Slow Animations.
Remember the yellow view is the root view of the scene, and is resized automatically by UIKit. On the other hand, the red view, a subview of the yellow view, is controlled by you.
When the animations are slowed down, notice that the yellow view’s rotation, width, and height transition over the duration of the rotation transition. Meanwhile, at the moment the rotation is triggered, the red view’s width and height change to their new values without transitioning. How can you make the red view’s resizing transition over time like the yellow view’s?
There’s an argument in the viewWillTransition method that’s the key to performing this transition: the transition coordinator.
The transition coordinator is generated when a scene transition begins, and handles animations of views during the transition. You can tell the transition coordinator to animate your views for you too, using the animate method.
The animateAlongsideTransition method accepts two arguments that are both closures, as explained in table 6.5.
Argument |
Description |
---|---|
animation | Any changes to properties within this closure will automatically animate for the duration of the transition. |
completion | This closure will be called after the animation is complete, and can be used for any cleanups, such as removing subviews. |
In general, the structure of a viewWillTransition method should look like the following listing.
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) 1 coordinator.animate (alongsideTransition: { (context) in 2 }) { (context) in 3 } }
For our ViewsInCode example, you could resize the red view and reposition the label within the animation block, as shown in the following listing.
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) 1 coordinator.animate (alongsideTransition: { (context) in self.redView.frame.size = CGSize(width: size.width, height: size.height / 2) 2 self.label.frame.origin.y = size.height / 2 }) { (context) in 3 } }
Run the app again with animations slowed down, and notice that this time the red view and the label animate smoothly to their new positions and dimensions.
Rather than programmatically adjusting the size and position of views in a scene at the moment a view transitions, developers may prefer to adjust them at the moment the scene’s root view’s layout is being updated.
Every view contains a flag that indicates that it requires updates to its layout. If this flag is set to true, it will be updated at the next appropriate moment in the run cycle. A view could be flagged as needing layout several times in the same cycle, but the actual layout process is only performed once.
The system can set this flag to true. When does the system flag that the root view’s layout needs updating? Here are common times:
You can also set the flag to true by calling the view’s setNeedsLayout method. If you do need to manually request the layout process to be performed immediately, this is possible as well, by calling the layoutIfNeeded method.
When the appropriate time arrives in the run cycle to update a view, and it’s flagged to require updates, three methods are called to lay out a view’s subviews (see figure 6.29).
The view controller first registers that it will lay out the subviews, then the view does the layout of subviews, and finally the view controller registers that it did lay out the subviews. You can override any of these methods to manually resize and reposition subviews. If the trigger to lay out involves a transition such as resizing a view, the transition coordinator will animate any changes made in these three methods as well.
To explore this alternative approach, you’ll use the viewWillLayoutSubviews method to update the position and size of the redView and label.
First, remove or comment out the viewWillTransition method from the previous section.
Override the viewWillLayoutSubviews method, and call its super method. Resize the red view and reposition the label. To see when this method is called, print a message to the console, as shown in the following listing.
override func viewWillLayoutSubviews() { 1 super.viewWillLayoutSubviews() 2 self.redView.frame.size = CGSize(width: view.frame.width, 3 height: view.frame.height / 2) 3 self.label.frame.origin.y = view.frame.height / 2 4 print("View will layout subviews") 5 }
Run the app on the simulator again, and rotate the app. You should see the red view resizing and the label repositioning, animated the way it was before.
The approach you decide to go with is up to you and the specifics of your app, but advantages do exist for repositioning and resizing when the layout updates:
Certain disadvantages to this approach are these:
Although certain developers prefer to adapt views programmatically, most iOS developers use programmatic repositioning and resizing of views as a last resort, useful for certain circumstances where auto layout isn’t sufficient, such as dynamic interfaces or customized animations.
In this chapter, you’ve looked at several approaches to building an adaptive layout:
Wow, that’s a long list of alternatives! How do you know which to use?
We covered some of the pros and cons of each. Beyond those, it comes down to personal preferences. Some users may prefer the granular control of making changes manually. Others may prefer the relative simplicity of autoresizing. Another group may prefer to have everything in code, whereas others like to work visually.
I generally lean toward auto layout. It can be complicated at first, but the time investment in getting familiar with it is worth it, and with practice the process of describing your layout using constraints will become easier. I also prefer to use Interface Builder where possible to visualize the interface, and more quickly recognize issues with my constraints.
That said, auto layout isn’t like working in a vacuum, and it’s a good idea to be familiar with other adaptive layout options. Different combinations of techniques can be used where appropriate. Dynamic designs, for example, are great candidates for working with adaptive layout in code.
We’re not done with adaptive layout yet! We’ll explore more ways to adapt interfaces in the next chapter. Though our layouts have adapted, they’ve still been similar on different devices and rotations. In the next chapter, you’ll make your apps adapt even more to their environment!
In this chapter, you learned the following: