Hour 16. Building Responsive User Interfaces


What You’ll Learn in This Hour:

Image How to make an application “responsive”

Image Using Auto Layout to enable automatic resizing and rotation

Image Creating UI elements programmatically for the ultimate control


You can use almost every iOS interface widget available, create multiple views and view controllers, add sounds and alerts, write files, and even manage application preferences, but until now, your applications have been missing a very important feature: responsive interfaces. The ability to create interfaces that display correctly regardless of your iDevice’s screen size or orientation is one of the key features that users expect in an application—and the key target of responsive interface design.

This hour’s lesson explores the Xcode Auto Layout system and a purely programmatic way of adding rotatable and resizable interfaces to your apps.

As you read this hour’s lesson, an important fact to keep in mind is that there are many different ways to solve the problem of responsive interface design. What works best for you may be entirely different than what I describe here.

Responsive Interfaces

Years ago, when I had my first Windows Mobile smartphone, I longed for an easy way to look at web content in landscape mode. There was a method for triggering a landscape view, but it was glitchy and cumbersome to use. The iPhone introduced the first consumer phone with on-the-fly interface rotation that feels natural and doesn’t get in the way of what you’re trying to do.

Over the past few years, Apple has gone wild with new device sizes and shapes. Does that mean you need a different storyboard for every single device variation? Nope! The iOS Auto Layout system makes it easy to adapt to different resolution displays and change your interface layout as your device resolution changes.

Although you should definitely plan to accommodate all sizes of the iPhone screen in your iPhone apps, the decision to handle rotation is entirely up to you. Consider how the user will be interfacing with the app. Does it make sense to force a portrait-only view? Should the view rotate to accommodate any of the possible orientations the phone may assume? The more flexibility you give users to adapt to their own preferred working style, the happier they’ll be. Best of all, enabling rotation is a simple process.


Tip

Apple’s user interface guidelines for the iPad strongly encourage the support of any orientation/rotation: portrait, left landscape, right landscape, and upside-down.


Enabling Interface Rotation

The projects that you’ve built so far have supported limited interface rotation, based on a single line of code in a single method in your view controller. This is added by default when you use one of Apple’s iOS templates.

When an iOS device wants to check to see whether it should rotate your interface, it uses the supportedInterfaceOrientations method in your view controller. The implementation of supportedInterfaceOrientations just returns a value that describes the supported orientation. To cover more than one orientation, you just return the list of constants you want to use, separated by the | character. With the initial releases of Xcode 6.x, accessing these constants requires us to tack rawValue on the end and to wrap the whole thing in a conversion to an integer—hopefully this nuttiness goes away in the future.

There are seven screen orientation support constants, as listed here:

Image Portrait: UIInterfaceOrientationMask.Portrait

Image Portrait Upside-Down: UIInterfaceOrientationMask.PortraitUpsideDown

Image Landscape Left: UIInterfaceOrientationMask.LandscapeLeft

Image Landscape Right: UIInterfaceOrientationMask.LandscapeRight

Image Any Landscape: UIInterfaceOrientationMask.Landscape

Image Anything But Upside-Down: UIInterfaceOrientationMask.AllButUpsideDown

Image Anything: UIInterfaceOrientationMask.All

For example, to allow your interface to rotate to either the portrait or landscape left orientations, you implement supportedInterfaceOrientations in your view controller with the code in Listing 16.1.

LISTING 16.1 Activating Interface Rotation


override func supportedInterfaceOrientations() -> Int {
    return Int(UIInterfaceOrientationMask.Portrait.rawValue) |
           Int(UIInterfaceOrientationMask.LandscapeLeft.rawValue)
}


The return statement handles everything. It returns the combination of the two constants that describe landscape left and portrait orientations.


Note

In addition to adding this method to your view controllers, you must also choose the orientations supported by your app by clicking the Device Orientation checkboxes in the application Deployment Info settings. This is described in the section “Setting Supported Device Orientations” in Hour 2, “Introduction to Xcode and the iOS Simulator.”


At this point, take a few minutes and go back to some of the earlier hours and modify this method in your view controller code to allow for different orientations. Test the applications in the iOS Simulator or on your device. For iPhone apps, try switching between devices with different horizontal and vertical dimensions displays.

Some of the applications will probably look just fine, but you’ll notice that others, well, don’t quite “work” in the different screen orientations and sizes, as shown in Figure 16.1 (ImageHop, from Hour 8, “Handling Images, Animation, Sliders, and Steppers”).

Image

FIGURE 16.1 Enabling an orientation doesn’t mean it will look good.

Everything we’ve been building has defaulted to a portrait design and, for the most part targeted a very specific screen height and width. So how can we create interfaces that look good regardless of the orientation and size of the screen? We obviously need to make some tweaks.

Designing Rotatable and Resizable Interfaces

In the remainder of this hour, we explore several different techniques for building interfaces that rotate and resize themselves appropriately depending on the device, or when the user changes the device’s screen orientation. Before we get started, let’s quickly review the different approaches and when you’d want to use them.

Auto Layout

The Xcode Interface Builder (IB) editor provides tools for describing how your interface should react when it is rotated or when the size of the screen can be variable. It is possible to define a single view in IB that positions and sizes itself appropriately—no matter what the situation—without writing a single line of code. This bit of Apple magic is called Auto Layout. It will be your best friend and worst enemy in creating responsive interfaces.

Using Auto Layout should be the starting point for all interfaces. If you can successfully define portrait and landscape modes in a single view in the IB editor, your work is done.

Unfortunately, Auto Layout alone doesn’t work well when there are many irregularly positioned interface elements. A single row of buttons? No problem. Half a dozen fields, switches, and images all mixed together? Probably not going to work.


Note

You’ve already used Auto Layout a teensy bit in Hours 10, 11, and 12 to create universal applications that present consistent views on the iPhone and iPad. In this hour, however, you’ll learn how the Auto Layout constraints you added actually worked.


Programming Interfaces

As you’ve learned, each UI element is defined by a rectangular area on the screen: its frame. If we programmatically define an application interface, we can make it into anything we want. Although this might sound challenging, it’s actually very simple and very effective for creating interfaces that vary dramatically between different sizes and orientations.

For example, to detect when the interface rotates, we can implement the method didRotateFromInterfaceOrientation in our view controller. The view controller variable property interfaceOrientation will tell us the current orientation of the interface by containing one of these four constants:

Image Portrait: UIInterfaceOrientation.Portrait

Image Portrait Upside-Down: UIInterfaceOrientation.PortraitUpsideDown

Image Landscape Left: UIInterfaceOrientation.LandscapeLeft

Image Landscape Right: UIInterfaceOrientation.LandscapeRight

The drawback to doing everything in code is that you are doing everything in code. It becomes very difficult to prototype interface changes and visualize what your final interface will look like. In addition, it is harder to adapt to changes in the future.

Swapping Views

A more dramatic approach to changing your view to accommodate different screen orientations is to use entirely different views for landscape and portrait layouts. When the user rotates the device, the current view is replaced by another view that is laid out properly for the orientation.

This means that you can define two views in a single scene that look exactly the way you want, but it also means that you must keep track of separate IBOutlets for each view. Although it is certainly possible for elements in the views to invoke the same IBActions, they cannot share the same outlets, so you’ll potentially need to keep track of twice as many UI widgets within a single view controller.


Note

To know when to change frames or swap views, you will be implementing the method didRotateFromInterfaceOrientation in your view controller. This method is called when the interface has finished changing orientation. If you want to react prior to the orientation change taking place, you could implement the willRotateToInterfaceOrientation:duration method.



Tip

Apple has implemented a screen-locking function so that users can lock the screen orientation without it changing if the device rotates. (This can be useful for reading while lying on your side.) When the lock is enabled, your application will not receive notifications about a change in orientation. In other words, to support the orientation lock, you don’t need to do a thing.


Size Classes

Size classes, covered in Hour 23, give us the best of all worlds. Using the techniques in this hour, we can create interfaces that resize or shift depending on orientation—but they aren’t focused on a particular device. Size classes enable us to combine the features of Auto Layout with the ability to have independent/unique views—all tied to a specific class of device (that is, the “size class”).

Of course, you’re welcome to build your application in whatever manner suits you. I recommend waiting until Hour 23 before starting a complex app that aims to take advantage of all of Apple’s unique device screen sizes.

Using Auto Layout

As already mentioned, Auto Layout enables us to build responsive interfaces without writing a line of code. Because of that, our first tutorial is a series of examples that you can follow along with, rather than a project that we build. I’ve included several sample storyboard files in this hour’s Storyboards folder, but you should definitely get yourself into IB and attempt the examples on your own.


Note

You’ll be making changes to the Empty.storyboard file to test several of the concepts in this hour’s lesson. I recommend copying this file and making changes to your copies. This will save you the time of having to create a new storyboard file (or deleting the contents of the current file) each time you want to try something new.

In addition, the examples in this section are all platform-agnostic; you can simulate the results on any iOS device in any orientation.


The Language and Tools of Auto Layout

When I see something with auto in the name, I expect that it is going to do things for me: automatically. Auto Layout lives up to this, but not without a reasonably long “getting to know you” phase. To use Auto Layout effectively, you must understand the terminology and know where to look for the tools. We spend the next few minutes getting into details, and then walk through a few examples that should get you started on the way to mastery. (Perhaps of archery or speed skating. Auto Layout if you’re lucky.)

Adding Basic Constraints

Auto Layout works by building a series of constraints for your onscreen objects. Constraints define placement of objects, distances between objects, and how flexible these relationships are. A vertical constraint for a button, for example, might be along the lines of “keep X many points of space between this object and the top of the screen” or “keep object Y vertically centered in the view.” Unfortunately, the language used for positioning can be a bit confusing; you may not be familiar with these terms:

Image Leading: The “front” or left side of an object.

Image Trailing: The “back” or right side of an object.

Image Superview: The view that contains an object. In our examples, this is the “view” that makes up the scene itself.


Note

Xcode must always have enough constraints defined for an object that it can determine its horizontal and vertical position at any point in time (regardless of the screen orientation/size). If you don’t add constraints of your own, Xcode transparently adds them for you when you build your project. This is very different from the initial releases of Xcode with Auto Layout support, which always added constraints. Now, with Xcode 6.x, you can build interfaces without worrying about constraints, then add them when you’re ready.


For example, try adding a button to the window in the Empty.storyboard file; make sure that it is located toward the top-left side of the view and that you’ve resized the button slightly. In its initial state, your button layout doesn’t display any constraints. That doesn’t mean that your layout won’t use them; it just means that Xcode will automatically add what it feels is appropriate when you build your project.

To add constraints that you can actually work with and adjust, you’ll use the Editor, Resolve Auto Layout Issues, Add Missing Constraints menu option (as shown in Figure 16.2). This adds constraints for the currently selected object. To add constraints to all objects in a scene, you can choose Add Missing Constraints under the All Views in View Controller heading instead. Go ahead and try either of these options on your scene with the button.

Image

FIGURE 16.2 Use the Add Missing Constraints menu option to add default constraints to your UI objects.

After you’ve added constraints, you’ll notice two visual changes. First, there are blue lines (representing the constraints) attached to your UI objects. In addition, a new Constraints entry appears parallel to the button in the document outline hierarchy, as well as a Constraints item within the button object itself, as shown in Figure 16.3.

Image

FIGURE 16.3 The Constraints elements represent the positioning relationships within a view or size constraints on the UI elements themselves.


Tip

If you ever feel like your constraint editing has gotten out of hand, you can use the Clear Constraints and Reset to Suggested Constraints options in the Resolve Auto Layout Issues menu to remove all existing constraints or to reset the object to the default constraints.


Navigating the Constraints Objects

Within the view’s Constraints object are two constraints: horizontal space and vertical space constraint. The horizontal constraint states that the left side of the button will be a certain number of points from the left edge of the view. This is known as the leading space (versus the space between the right side and the edge of the view, which is called the trailing space). The vertical constraint is the distance from the top of the view to the top of the button.

What constraints are added depend on where the object is in relation to its containing view. When you use Xcode to automatically add constraints, it does its best to choose what makes the most sense. If you position a button near the bottom of the view and add suggested (or missing) constraints, for example, Xcode will add a constraint for the distance between the bottom of the button and the bottom of the view.

Constraints, however, are more than just entries that tie an object to the view it is within. They can be flexible, ensuring that an object maintains at least or at most a certain distance from another object, or even that two objects, when resized, maintain the same distance between one another.


Tip

Alignment of objects is also a constraint, and is managed using the alignment tools found under the Editor, Align menu. In other words, when you set vertical or horizontal alignments, you’re actually configuring constraints that will be maintained regardless of the device’s screen size or orientation! We’ll use these later this hour.


Constraints that set a specific size or distance between objects are called pinning. The flexibility (or inflexibility of a constraint) is managed by configuring a relationship.

This brings us to the second constraint object in the document outline: constraints on the object itself, not on its positioning. When you’ve resized a user interface (UI) element or set constraints on how the content within it is positioned, Xcode will add a constraint object inside the UI element entry in the document outline. These constraints determine the width or height of the element (or both). Not surprisingly, when you’ve got dozens of components in your UI, you’ll have lots of constraints in your document outline.

Viewing and Editing Constraints via the Size Inspector

While sorting through constraints in the document outline can be a pain, you can quickly view all the constraints applied to an element (whether located in the Constraints object on the view the element is in, or within the UI element itself) within the Size Inspector (Option-Command-5). For example, if you’ve added a resized button to the storyboard, then used Xcode to add the default missing constraints, opening the Size Inspector should look something like Figure 16.4. Here horizontal and vertical positioning for the button are represented as constraints, as well as the width of the button.

Image

FIGURE 16.4 Use the Size Inspector to view your constraints.


Tip

To quickly see which constraint is which, hover your cursor over one of the constraints in the Size inspector. The corresponding constraint highlights in the Interface Builder editor. This is demonstrated with the Bottom Space constraint in Figure 16.4.


Use the Edit button to the right side of each constraint to select and edit it. For example, if you pick a width constraint and click Edit, Xcode shows a popover with simple settings for adjusting it, as shown in Figure 16.5.

Image

FIGURE 16.5 Click edit to display a popover and quickly edit a constraint.

For full editing capabilities, however, you’ll want to double-click the constraint, which jumps you into the Attributes Inspector. Here you can see and adjust all the details of the constraint, as demonstrated in Figure 16.6. The Attributes-Inspector approach is what I’ll be showing throughout the hour.

Image

FIGURE 16.6 Use the Attributes Inspector to edit constraints.

The First Item and Second Item drop-downs are used to configure what part of each object involved in a constraint is being considered. A button that is constrained to the top layout guide, for example, might have the constraint set on its top or bottom—or maybe even its center. In general, these aren’t something you’ll often touch, because the correct pieces parts are chosen for you when you create the constraints.

The Relation drop-down determines what the constraint is trying to do. Is it trying to keep a distance or size equal to a value? Greater than or equal to it? Less than or equal? You choose the relation that gives your interface the flexibility you need. With each relation, you can also set a constant. This is a measurement, in points, that the relationship will be maintaining.

You’ll also notice that a Priority value, Multiplier, a Placeholder check box, and a Standard check box (sometimes) appear in the editor. The Priority slider determines how “strong” the constraint relationship is. For example, there may be instances when multiple constraints must be evaluated against one another. The priority setting determines the importance of any given constraint. A value of 1000 is a constraint that is required. If you move the slider, Xcode shows a description of what to expect at a given constraint priority.

The multiplier is exactly what its name suggests—a value that is used for multiplying. Multiplying what? The second item involved in a relationship. Imagine you have two buttons that include a constraint for their widths to be equal. If you’d like the first button’s width to be maintained as twice as large as the second, you would add a multiplier of 2, as follows:

Button1.width = 2 (the multiplier) × Button2.width

The Standard check box (not visible in Figure 16.6) lets Xcode use its internal database of spacing to set the recommended space between two objects. This, in many cases, is preferred because it ensures a consistent interface.

The last setting, the Placeholder check box, is used if you plan to resize or move the UI object in your code. If you check this option, you can use Auto Layout tools to set up your UI element, but the constraints will be removed when your application runs.


Caution: Installed? What’s That?

For now, don’t mess with the Installed check box or the very light + buttons you’ll see when inspecting constraints. These features are related to size classes, which you’ll learn about in Hour 23.


In a few minutes, we’ll make use of the editor to configure a few relationships. Trust me, it makes more sense once you get started.


Tip

Constraints can also be edited by selecting them in the design view and opening the Attributes Inspector, double-clicking opens a quick-editing popover.

If you’re good at clicking on thin lines, you can do this without going into the Size Inspector. I personally prefer starting by selecting the UI element, viewing the constraints in the Size Inspector, and then jumping into editing from there.


Content Hugging and Content Compression Resistance

When viewing an interface object with constraints in the Size Inspector, you noticed settings for Content Hugging Priority and Content Compression Resistance Priority. These features are related to Auto Layout, but what do they do?

These settings control how closely the sides of an object “hug” the content in the object and how much the content can be compressed or clipped. Imagine a button that you want to expand horizontally, but not vertically. To allow horizontal expansion, you’d set horizontal hugging as a low priority. To keep it from growing vertically, you set vertical hugging to a high priority.

Similarly, the content (the button label) should not be compressed or clipped at all, so the content compression resistance settings for both horizontal and vertical compression should be very high priority.

You will not often need to adjust these settings beyond their defaults, which IB adds for you.


Tip

UI objects like buttons have intrinsic constraints that do not appear in the interface. These are the sizes that Xcode determines the object should have in order to display your content (such as a button label or button image). Xcode will automatically try to maintain these constraints unless you override them with your own user constraints and content compression/hugging settings. Other objects, such as switches, cannot resize at all, so there’s no tweaking that will cause them to violate their intrinsic constraints. Text views, image views, and other “generic” content holders, however, will happily resize with no difficulty. You’ll see this behavior in a few minutes.


Understanding and Correcting Constraint Errors

When I am reading a book, I tend to skip over anything that talks about errors. Why? Because I don’t make mistakes... and I’m sure you don’t either. Unfortunately, no matter how careful you are with Auto Layout, you’re going to see errors—lots of them—and they’re going to look ugly.

For example, we’ve been looking at a button in the Empty.storyboard. We added the button, added the constraints, and everything was fine. Now, try dragging the button to a new position in the window and see what happens. As soon as you drag the button, you’ll see constraints appear drawn in orange, along with a dotted line where your button was originally located, as shown in Figure 16.7.

Image

FIGURE 16.7 Constraint errors appear if you do anything that would change the existing constraints.

The solid lines (and numbers on the lines) show the changes in the original constraints. A +5 on a vertical constraint, for instance, means the object has shifted down 5 points from the original constraint. A –5 on a horizontal constraint indicates a shift to the left. The dotted line shows the frame of the original object.

What’s important to understand here is that even though you’ve moved the object in IB, the original constraints remain—so your object is now considered “misplaced” in Xcode.

In addition to highlighting constraints in another color, Xcode also shows Auto Layout errors in the document outline. When you moved the button an orange indicator appeared by the scene in the object hierarchy; you can see this in Figure 16.7. To get a description of the error, click the indicator icon in the document outline. The display refreshes to show the error, as shown in Figure 16.8.

Image

FIGURE 16.8 Looks like we have a Misplaced Views error.


Tip

If you hover your mouse over the main error heading (in this case, Misplaced Views), a small i icon will appear. Clicking this icon will show a plain English description of what the error means and ways to correct it.


To fix the error, click the error indicator icon beside the UI object with an error that you want to fix. Xcode displays a list of possible fixes. Try this with the button example. Your display should resemble Figure 16.9.

Image

FIGURE 16.9 Choose how you want to fix the detected errors.

You have three options for automatically fixing placement errors:

Image Update Frame: Moves the control so that it fits the constraints that are defined. In the case of the button example, this puts it back where it came from.

Image Update Constraints: Changes the existing constraints to match the current positioning of the object.

Image Reset to Suggested Constraints: Adds new constraints to the object (eliminating the old) to match its current position. This uses the best constraints for the current position (in contrast to Update Constraints, which changes the original constraints to fit, even if they aren’t the best choice for the position).

Choose Update Constraints or Reset to Suggested Constraints and you should be back in business. Check the Apply to All Views in Container check box to apply to all similar errors, if desired. Click Fix Misplacement to update the storyboard accordingly.


Tip

From the menu bar, you can find the options for fixing Auto Layout issues under the Editor, Resolve Auto Layout Issues. Within this menu, you’ll also find selections for applying a fix to your entire view controller, not just a single control.


Although this scenario (in my opinion) is the most common that you will encounter, other constraint errors are possible. In some cases, you may have multiple conflicting constraints (two constraints pinning the height to different values, for example)—in which case, you’ll be prompted to delete one of the conflicts.

On the other end of the spectrum, if you don’t have enough constraints to determine an object’s position, you will see an error for missing constraints. Clicking through the error indicators will present you with the option to add the missing constraints, as shown in Figure 16.10.

Image

FIGURE 16.10 Find and fix your Auto Layout errors, whatever they may be.


Caution: Align Your Objects. Create an Error. Neato!

Early in the book (Hour 5, “Exploring Interface Builder”), I demonstrated how you can use the alignment tools to align UI elements. Using alignment adds constraints for your objects as if they had moved, but it doesn’t move them. The result? Errors. Lots and lots of errors.

The easiest way to deal with alignment is to add missing (default) constraints, align, and then update or reset the constraints.


Before we put what we’ve learned into practice, I want to make one thing clear: There is no “one way” to build interfaces with Auto Layout, but there is a mindset that will be helpful as you build your own designs. Specifically, start by identifying the interface elements that you want to anchor somewhere on the screen, regardless of orientation or screen size. From there, begin looking for the relationships between those elements and the other objects that make up your interface. Building relationships between objects, rather than trying to tie each element to specific coordinates within a view, is a good tactic for successful UI design.

Example One: Centering Constraints

Let’s look at a very simple example: a button that stays centered regardless of the screen size of your device, including rotations between portrait and landscape orientations. To do this, begin by adding and positioning the button:

1. Open a copy of the Empty.storyboard file and add a button to the scene. Resize the button slightly so that it is larger than the default size.

2. First, we’ll pin the width and height. Without a width and height set, the object won’t know whether it should grow, shrink, or move to center itself during a rotation. Pinning a width and height makes centering unambiguous. Select the button, then choose Editor, Pin, Width from the menu bar.

3. Reselect the button, and then choose Editor, Pin, Height. Yes, you have to keep reselecting the button. Annoying, isn’t it? You’ve now set a width and height that will be maintained regardless of the orientation.

4. To set the Align constraints, select the button, then choose Editor, Align, Horizontal Center in Container.

5. Repeat the step 5 alignment, this time aligning to the Vertical Center in Container.

6. For the last step, select the button one more time, then choose Editor, Resolve Auto Layout Issues, Update Frame. Your layout should now be finished and resemble Figure 16.12.

Image

FIGURE 16.12 The final constraints for a nicely centered button.


Note

Notice the little preview at the top of the list of constraints in the Size Inspector? This view gives you a quick visual representation of the constraints applied to an object, and allows you to click the lines to immediately select the corresponding constraint and hide all the others.


Switch to the assistant editor (View, Assistant Editor, Show Assistant Editor), and select Preview from the path bar above the editor (click where it initially shows Automatic). Use the control at the bottom of the preview display to rotate the display; use the + button in the lower-left corner of the preview to add additional device previews. Assuming your constraints are correct, the button should also properly center itself regardless of your settings, as demonstrated in Figure 16.13.

Image

FIGURE 16.13 The button centers itself, regardless of orientation, screen size, and device.

That was pretty simple, wasn’t it? Of course, not all your applications will consist of just a single button (only the best ones). For that reason, we cover a few more examples, starting with resizing controls.


Tip

I highly recommend (if your screen space allows) turning on the assistant editor’s Preview feature while playing with Auto Layout. You will be able to see the changes you make take effect in real time (which makes debugging constraints a much easier process).


Example Two: Expanding Controls

A common design problem is where you have a series of controls on the screen and they need to expand to fill the size of the screen. As an example, let’s create an interface with two text views at the top and a button at the bottom. The button should always remain anchored at the bottom, but should grow horizontally to fill the width of the display. The text views should grow or shrink vertically, and expand horizontally to fill the screen.

Creating the Interface

Starting from a fresh copy of Empty.storyboard, follow these steps:

1. Drag a button to the storyboard, positioning it at the bottom center of the view using the guides that appear while dragging.

2. Resize each side of the button by dragging toward the side of the view, and then release when you see the blue guide appear.

3. Select the button, and then from the menu bar, choose Editor, Resolve Auto Layout Issues, Add Missing Constraints.

By using the guides in steps 1 and 2, Xcode knows that when it adds the missing constraints, it should use the default margins for pinning the button. You should see three constraints that anchor to the side and bottom margins of the view, as shown in Figure 16.14.

Image

FIGURE 16.14 These three constraints make the button resize and stay anchored at the bottom, regardless of the screen size or orientation.

The constraints that have been created are a good start for the button. These ensure that the sides of the button are always an equal distance from the sides of the view, regardless of the orientation. The same goes for the distance from the bottom of the view to the bottom of the button.


Tip

The Pin menu at the bottom of the IB Editor lets you quickly update constraints on whatever object is selected in the editor, or add new constraints. This tool gives you yet another way to add and edit constraints in Xcode.


Next, add two text views to the scene:

1. Drag two text views to the scene, stacking one on top of the other.

2. Resize the text views to fill the scene horizontally, until the blue guides appear at the margins.

3. Size the text views vertically until the guides appear to keep the views from overlapping the button, one another, or the top margin of the scene.

4. Select both text views. And then from the menu bar, choose Editor, Resolve Auto Layout Issues, Add Missing Constraints.

5. Set the background color of the text views to something other than white so that they are easier to see. Your finished view should look like Figure 16.15.

Image

FIGURE 16.15 You’ve added three UI elements and quite a few constraints.

Depending on how you added and sized for your text views, you’ll notice that each of them has a variety of constraints that tie them to the sides of the screen, the top (or bottom) of the screen, and maintain spacing between each other and the button.

You’ll also see that one of the text views will have a height constraint applied. We’ll be working with this constraint in a few minutes, so find it now.

Why is there only one height constraint? Because as long as one is defined, the other text view can have a relative height calculated from all the other constraints. (If this sounds confusing, think of two lengths of string that together equal 1 foot. If one string is 7 inches long, you can calculate the second length of string... I hope.)

Setting the Constraints

Try resizing and rotating the display using the assistant editor’s Preview feature. What you’ll notice is that resizing it vertically (switching between 3.5-inch and 4-inch screens) likely works okay. What definitely doesn’t is rotating to landscape. In landscape mode, one of the text views will shrink so much that it isn’t even visible. To make the scene work right, you need to set some additional constraints.

Specifically, you need to accomplish these things:

1. Pin the height of the button so that it can’t get squished by one of the text views.

2. Choose the text views to be the first to shrink when the size changes. We set this view so that its height is greater than or equal to the smallest size we want it to reach. I will use the top view for this purpose and set its minimum height to 100 points or greater.

3. Set a fixed height on the bottom text view (I’ll use ~250 points) so that it doesn’t change size. But, we need to set its priority to less than 1000 (the default). This lets the text view change sizes in an extreme situation. That “extreme situation” would otherwise occur in landscape mode where the top view would try to maintain a height of 100 points but the bottom view’s fixed height of 250 would force a condition where both can’t hold.

Begin by selecting the button. Then, from the menu bar, choose Editor, Pin, Height. Simple enough!

Next, take a look at which of your text views already has a height constraint. We need to make sure that both do. So, select the one that doesn’t and choose Editor, Pin, Height. You’re now ready to add some constraint logic.

Select the top text view; then use the Size Inspector to select and edit the height constraint.

Using the Relation drop-down, choose Greater Than or Equal. Set the constant to 100 (the text view must be 100 points in height or greater) and leave the priority alone. Your configuration should look like Figure 16.16.

Image

FIGURE 16.16 Set the relation for the height constraint.

Now the top text view will shrink when it needs to, but it can also grow; in fact, until a constraint is set on the bottom text view, it can grow too large, pushing the bottom text view until it is too small in any orientation.

Select the bottom text view and again edit its height constraint. (Add one if it isn’t already there.) Make sure that the relation is set to Equal with whatever constant Xcode added, as shown in Figure 16.17. This time, however, set the priority down to 999. This makes the constraint turn into a dotted line (visible in Figure 16.17) showing that the constraint will try to be maintained, but, if necessary, it can resize to avoid violating other constraints.

Image

FIGURE 16.17 Set a height constraint with a priority lower than 1000 to enable the text view to change sizes if absolutely necessary.

Go ahead and try resizing the view between iDevice screen sizes and rotating to landscape mode. Everything should work like a charm, with the top text view resizing when needed and the bottom text view resizing when it absolutely has to.


Caution: What If It Doesn’t Work?

If something doesn’t appear to be working, it’s likely because there are extra height constraints. Look for multiple height constraints (or any others that look out of place) and delete any duplicates that may be conflicting.



Tip

Combining multiple constraints with variable relationships, like this, is what you need to do to accommodate complex interfaces. You can also manually add all your constraints using the Pin options—instead of getting the default set with Add Missing Constraints.

Don’t be shy about trying different relationships and priorities to see how they interact. This, for example, is a great place to try pinning Heights Equally with both the text views selected. You can even modify this constraint with a multiplier so that one text view is always sized proportionally to the other.

There isn’t a “recipe” for making effective Auto Layout designs. Spend a few hours playing with the tools—laying out interfaces in different ways. Hands-on is the only way you’ll get the experience you need to be comfortable with Auto Layout.


Example Three: Variable and Matching Sizes

Okay, one more quick exercise to round things out. This time, we add three buttons in a horizontal line, and we add enough constraints to make the buttons all resize equally when we shift to landscape or a different device.

Create a new copy of the Empty.storyboard document, and then drag a new button into the middle of the display. Size the button to be a bit under 1/3rd of the screen width (doesn’t matter what the simulated size of the display is)—you’re going to be placing three in a horizontal row. Next, set a background color for the button’s view so you can more easily see the width.

Copy your button to create a total of three, and label them Button A, Button B, and Button C. Use the guides to align them in the vertical center of the display, as seen in Figure 16.18.

Image

FIGURE 16.18 Add three buttons, nothing more.

You now know enough to make one of the buttons resize, but how could we constrain them so that all the buttons resize to fill in the screen in landscape mode? These are the steps constraints that I came up with:

1. Start by manually positioning the buttons where you’d like them—using the guides wherever possible.

2. Align all the buttons to the vertical center in the container. This keeps them centered vertically on the screen.

3. Align the middle button to the horizontal center of the container; it should always remain in the middle of the screen.

4. Pin the Leading Space of Button A to the superview (the edge of the view).

5. Pin the Trailing Space of Button C to the superview.

6. Pin the Horizontal Spacing between Button A and Button B.

7. Pin the Horizontal Spacing between Button B and C. (If we stopped here, the center button (Button B) would stretch while the other buttons would stay the same size.)

8. Pin the buttons so that they all share the same width. (Select all the buttons, and then choose Editor, Pin, Widths Equally.)

9. Resolve any errors that appear by choosing Editor, Resolve Auto Layout Issues, Update Constraints.

Use the Editor menu or the IB editor buttons to add these constraints to your storyboard; you should know how to do it by now. There is no need to adjust the priority or relationships in this case. Your finished layout will hopefully be similar to Figure 16.19. (One of the = constraints may be on a different button. This is fine.)

Image

FIGURE 16.19 Your scene should have constraints much like these.

Switch to the assistant editor and open a preview. When you view your design in landscape or on varying devices, all the buttons should resize equally and fill in the space across the screen, as shown in Figure 16.20.

Image

FIGURE 16.20 The buttons’ constraints force them to spread out across the screen and match sizes.

Programmatically Defined Interfaces

In the previous example, you learned how the IB editor can help quickly create interface layouts that look as good horizontally as they do vertically and that can resize between the various iDevice displays. Unfortunately, in plenty of situations, IB can’t quite accommodate your dream UI. Irregularly spaced controls and tightly packed layouts rarely work out the way you expect. You may also find yourself wanting to tweak the interface to look completely different—positioning objects that were at the top of the view down by the bottom and so on.

In cases like this, consider implementing the UI (or portions of it) completely in code. But, what about the nice and neat drag-and-drop approach? Well, using code isn’t as convenient as drawing your interface in IB, but it isn’t difficult. We’re going to move to a project that is the exact opposite of our last few examples—rather than building a responsive interface without code, we’re going to only use code to create an interface.

Implementation Overview

In this tutorial, we create a very simple application with three UI elements: two buttons (Button A and Button B) and a label. The buttons trigger a method to set the label to the title of the button. Most important, however, is that the interface reacts properly to orientation and size changes.

In the portrait orientation, the buttons are drawn with the label sandwiched between them. In landscape, the buttons move closer to the bottom, and the label repositions above them. The final output will resemble Figure 16.21.

Image

FIGURE 16.21 Buttons resize and reposition appropriately (all handled in code).

Take note that the positioning of the buttons and the label cannot be handled in Auto Layout (at least not with our current toolset). When you encounter issues that can’t be solved in Auto Layout, there’s no harm in coding your way out of the forest.

To handle the rotation and resizing of the objects, we use the bounds of the scene’s view. This gives us the width and height of the device corresponding to whatever orientation it is in. We then use these values to position the UI elements on the screen. By basing the positioning on the bounds, the size of the device screen and orientation are largely irrelevant, as you’ll soon see.

Setting Up the Project

Unlike the previous example, we can’t rely on pointing and clicking for the interface, so there is a bit of code in the project. Once again, create a new single-view iOS application project, targeting Universal devices, and name it AllInCode.

Planning the Variables and Connections

In this exercise, you manually resize and reposition three UI elements: two buttons (UIButton) and one label (UILabel). Although we aren’t creating these with outlets, we define variable properties for them: buttonA, buttonB, and theLabel should suffice.

We also implement a method: handleButton, which updates the onscreen label to show the title of a button that was tapped. Like the variables, this won’t be declared using IB, but we’ll be using it just like an IBAction. We also add two additional methods, initInterface and updateInterface, to handle setting up and updating the interface, respectively. These will be triggered by a change in orientation, so our next step is to set up the project to properly handle orientation changes.

Enabling Orientation Changes

For this project, enable support of all orientations. To do this, start by updating the project summary (click the blue project icon at the top of the project navigator) and select all the device orientations within the Deployment Info section. Next, add the supportedInterfaceOrientations method to ViewController.swift and have it return the constant UIInterfaceOrientationMask.All, as shown in Listing 16.2.

LISTING 16.2 Supporting All Interface Orientations


override func supportedInterfaceOrientations() -> Int {
    return Int(UIInterfaceOrientationMask.All.rawValue)
}


Programming the Interface

We’ve now reached the point in the project where normally I’d say, “Let’s design the interface.” This time, however, there isn’t going to be a visual design, just code. In fact, you’ve already seen the two screenshots that accompany this project, so if you’re just skimming pictures, you’d better flip through a few more pages.

Defining Variables and Methods

We start by defining the objects that the view controller will be using. Recall that we’re adding three objects: buttonA, buttonB, and theLabel. Because we can define these at the application start, we can actually add them as constants.

Edit ViewController.swift, adding the constant definitions below the class block, as shown here:

let buttonA: UIButton = UIButton.buttonWithType(UIButtonType.System) as UIButton
let buttonB: UIButton = UIButton.buttonWithType(UIButtonType.System) as UIButton
let theLabel: UILabel = UILabel()

Notice that I’m initializing the objects, not just declaring them. The buttons are created with the type UIButtonType.System—the standard type of button we get when adding buttons through Interface Builder.

Initializing the Interface Objects

The next step is to add the initInterface method to ViewController.swift. The purpose of this method is to configure all the interface elements (the two buttons and the label) so that they’re ready to be added to the interface, but not display them just yet.

By keeping the display logic separated from the initialization logic, we can build a method that can be invoked at any time to update the interface. This method, aptly named updateInterface, is called at the end of the initInterface and anytime interface rotation is sensed.

Add the initInterface method from Listing 16.3 to ViewController.swift.

LISTING 16.3 Prepare the Interface (But Don’t Display It Yet)


 1: func initInterface() {
 2:     buttonA.addTarget(self, action: "handleButton:",
 3:         forControlEvents: UIControlEvents.TouchUpInside)
 4:     buttonA.setTitle("Button A", forState: UIControlState.Normal)
 5:
 6:     buttonB.addTarget(self, action: "handleButton:",
 7:         forControlEvents: UIControlEvents.TouchUpInside)
 8:     buttonB.setTitle("Button B", forState: UIControlState.Normal)
 9:
10:     theLabel.text="Welcome"
11:
12:     updateInterface()
13: }


This might be the first time we’ve manually created a number of UI elements, but because you’ve been working with these objects and adjusting their variable properties for hours, this code shouldn’t seem completely foreign.

Lines 2–3 set the action for buttonA. The addTarget:action:forControlEvents method configures what will happen when the Touch Up Inside event occurs for the button—in this case, calling the method handleMethod. This is exactly the same as connecting a button to an IBAction in IB.

Line 4 sets the title for the button to Button A.

Lines 6–9 repeat the same process for Button B (buttonB).

Lines 10 initializes the label (theLabel) with the default text Welcome.

Lastly, line 14 invokes the updateInterface method so that the newly defined user elements can be placed on the screen. So, what do we do now? Implement updateInterface.

Implementing the Interface Update Method

The updateInterface method does the heavy lifting for the application. It checks to see what the current orientation is, and then it draws content based on the view’s bounds variable property. By basing the drawing on the height and width contained within bounds, you can scale to any screen size at all.

For example, consider this code snippet:

let screenWidth: CGFloat = view.bounds.size.width;
let screenHeight: CGFloat = view.bounds.size.height

This grabs and stores the current screen width and height in the constants screenWidth and screenHeight. The dimensions and position of UI objects are determined by their frame, which is a variable of type CGRect. To set the frame of a button named theButton so that it filled the top half of the screen, I’d write the following:

theButton.frame=CGRectMake(0.0,0.0,screenWidth,screenHeight/2)

The first two values of CGRectMake (which create a CGRect data structure) set the origin point at 0,0. The second two parameters determine the width and height of the CGRect. Using screenWidth sets the button to the same width of the screen, and screenHeight/2 sets the height of the button to half the height of the screen. In an actual implementation, you want to include some margin around the edges. This is why you’ll see +20 and other values tacked onto my coordinates. Speaking of which, go ahead and implement updateInterface, as shown in Listing 16.4. When you’re done, we step through the code.

LISTING 16.4 Implementing updateInterface


 1: func updateInterface() {
 2:     let screenWidth: CGFloat = view.bounds.size.width;
 3:     let screenHeight: CGFloat = view.bounds.size.height
 4:
 5:     for subview in view.subviews {
 6:         subview.removeFromSuperview()
 7:     }
 8:
 9:     if interfaceOrientation == UIInterfaceOrientation.Portrait ||
10:         interfaceOrientation == UIInterfaceOrientation.PortraitUpsideDown {
11:             buttonA.frame=CGRectMake(20.0,20.0,screenWidth-40.0,
12:                                     screenHeight/2-40.0)
13:             buttonB.frame=CGRectMake(20.0,screenHeight/2+20,
14:                                     screenWidth-40.0,screenHeight/2-40.0)
15:             theLabel.frame=CGRectMake(screenWidth/2-40,
16:                                     screenHeight/2-10,200.0,20.0)
17:     } else {
18:         buttonA.frame=CGRectMake(20.0,60.0,screenWidth-40.0,
19:                                     screenHeight/2-40.0)
20:         buttonB.frame=CGRectMake(20.0,screenHeight/2+30,
21:                                     screenWidth-40.0,screenHeight/2-40.0)
22:         theLabel.frame=CGRectMake(screenWidth/2-40,20.0,200.0,20.0)
23:     }
24:
25:     view.addSubview(buttonA)
26:     view.addSubview(buttonB)
27:     view.addSubview(theLabel)
28:
29: }


Lines 2–3 grab and store the current screen size in screenWidth and screenHeight.

Lines 5–7 loops through all the interface elements in our view, and removes them. This clears out our current interface settings prior to setting their new sizes and locations.

Lines 9–10 check the interfaceOrientation of the view controller, and, if it is in one of the portrait orientations, lines 11–16 are executed. Otherwise, lines 18–22 are evaluated. These blocks both have the same purpose: defining the frame for each of the UI elements (buttonA, buttonB, and theLabel).

Lines 11–16 define positions for the buttons so that there are margins on the edges of the screen and a space in the middle for the label. Lines 18–22 position the buttons lower on the screen and put the label at the top. The margins and spacing I used is completely arbitrary. You can try changing these values around to see what effect they have.

Finally, lines 25–27 add the buttons and label to the view so that they are visible onscreen.

Everything is now in place for the interface, but we need to take care of three small tasks before the project is complete. First, we need to make sure that the interface is drawn when the application first loads. Second, the interface must update when an orientation change occurs. Third, we need to implement handleButton to update the label when the buttons are pressed.

Drawing the Interface When the Application Launches

When the application first launches, there isn’t an orientation change to trigger the interface to be drawn. To make sure there is something on the screen, we need to call initInterface when the application loads. Add this to viewDidLoad, as shown in Listing 16.5.

LISTING 16.5 Initializing the Interface When the Application Loads


override func viewDidLoad() {
    super.viewDidLoad()
    initInterface()
}


We’re getting closer. The application will now initialize and display the interface, but it still can’t adapt to a change in orientation.

Updating the Interface When Orientation Changes

To handle orientation changes, the application needs to call updateInterface within an implementation of didRotateFromInterfaceOrientation.

Add didRotateFromInterfaceOrientation to ViewController.swift, as shown in Listing 16.6.

LISTING 16.6 Handling Rotation in didRotateFromInterfaceOrientation


override func didRotateFromInterfaceOrientation
        (fromInterfaceOrientation: UIInterfaceOrientation) {
    updateInterface()
}


Handling the Button Touches

The last piece of the puzzle is implementing handleButton so that it updates the onscreen label with the label of the button being touched. This is just a single line, so add Listing 16.7 to the view controller, and you’re done.

LISTING 16.7 Handling Button Touches


func handleButton(theButton: UIButton) {
    theLabel.text = theButton.currentTitle
}


The one line of the implementation uses the theButton parameter (the UIButton touched) to grab the title of the button and set the label to the title.

Building the Application

Build and run the application. It should rotate and resize with no problem. What’s more, because all of the interface layout was based on the height and width of the view, this same code will work, without changes, on any device you choose to run it on.

I hope this didn’t scare you too much. The purpose of this exercise was to show that responsive and flexible interfaces can be accomplished in code without it being too much of a hassle. The biggest challenge is determining how the controls will be laid out and then coming up with the CGRectMake functions to define their locations.

Further Exploration

Although we covered several different ways of working with interface rotation, you may want to explore additional features outside of this hour’s lesson. Using the Xcode documentation tool, review the UIView instance methods. You’ll see that there are additional methods that you can implement, such as willAnimateRotationToInterfaceOrientation:duration, which is used to set up a single-step animated rotation sequence. What’s more, you can combine these methods with segues—programmatically triggering a segue to a new scene when an orientation event occurs.

Later in the book (Hour 23), we’ll look at tools for developing Universal Applications that can completely adapt to different devices and orientations. The foundation for using these advanced tools, however, is a solid understanding of Auto Layout and how user interfaces are built and displayed. It’s a good idea to practice the techniques discussed in this hour because they will form the basis for everything you build.

Summary

iDevices are all about the user experience—a touchable display, intuitive controls, and now, rotatable and resizable interfaces. Using the methods described in this hour, you can adapt to almost any type of size or rotation scenario. To handle interface size changes without a line of code, for example, you can take advantage of the Auto Layout. For more complex changes, however, you might want to programmatically define your onscreen elements, giving you absolute control over their size and placement. Finally, for a good balance in flexibility, you could create multiple different views and swap them as the device rotates.

By implementing rotation and size-aware applications, you give your users the ability to use their devices in the way that feels most comfortable to them.

Q&A

Q. Why don’t many iPhone applications implement the upside-down portrait mode?

A. Although there is no problem implementing the upside-down portrait orientation using the approaches described in this hour, it isn’t recommended. When the iPhone is upside-down, the Home button and sensors are not in the “normal” location. If a call comes in or the user needs to interact with the phone’s controls, the user will need to rotate the phone 180 degrees—a somewhat complicated action to perform with one hand.

Q. How do I get the controls in application XYZ to behave using Auto Layout?

A. This is a difficult question with no clear answer because there may be dozens of ways of implementing constraints that have the desired effect. I have implemented Auto Layout constraints in all my sample projects. You may want to take a look at those to get an idea of how I solved resizing problems. (Landscape, however, is up to you!)

Workshop

Quiz

1. To correct a constraint error after moving a view, you would likely apply which of the following as a fix?

a. Update Frame

b. Remove Frame

c. Clear Frames

d. Remove Constraints

2. Which method is executed after an interface rotation has taken place?

a. didRotateFromInterfaceOrientation

b. didMoveFromInterfaceOrientation

c. didSwitchFromInterfaceOrientation

d. changedInterfaceOrientation

3. How many different orientations may be returned by the didRotateFromInterfaceOrientation method?

a. 1

b. 2

c. 3

d. 4

4. Apple’s system for creating interfaces that adapt to different-sized displays is called what?

a. Auto Layout

b. Springs and Struts

c. Flow System

d. Smart Grid

5. You can define a proportional relationship between object sizes using which constraint property?

a. Value

b. Constant

c. Multiplier

d. Priority

6. The space from the front of one object to the edge of another is called what?

a. Trailing space

b. Front space

c. Preface space

d. Leading space

7. If you use a constraint to set the width of an object to a specific value, you are doing what to the object?

a. Setting

b. Pinning

c. Forcing

d. Dimensioning

8. To determine the point in the middle of a screen (X,Y), what logic should you use?

a. screen width/2, screen height/2

b. screen width*2, screen height*2

c. screen width-screen height, screen height-screen width

d. screen width-screen height, screen height-screen width

9. To programmatically set an action for a button, you can use which of the following methods?

a. addButton:action:forControlEvents

b. addSelector:forControlEvents

c. addAction:forControlEvents

d. addTarget:action:forControlEvents

Answers

1. A. Misplaced views are often fixed using Update Frame.

2. A. Immediately after an interface rotation has taken place, the method didRotateFromInterfaceOrientation is called.

3. D. A total of four orientations are available: Landscape Left, Landscape Right, Portrait, and Portrait Upside-Down.

4. A. The Auto Layout system is used in OS X and iOS to enable developers to create responsive interface designs.

5. C. The Multiplier setting for a constraint can be used to create a proportional relationship between interface elements.

6. D. The leading space is the amount of space from the edge of one object to the front of another.

7. B. The act of setting a dimension or distance constraint is known as pinning.

8. A. Divide the screen’s width and height by 2 to locate a point in the exact center of the screen.

9. D. Use the addTarget:action:forControlEvents to programmatically configure the actions for a button.

Activities

1. Using what you’ve learned about programmatically defining an interface, try rebuilding an early exercise all in code—without using Interface Builder at all.

2. Return to an earlier lesson and revise the interface to support multiple different orientations and screen sizes. Use any of the techniques described in this hour’s exercises for the implementation.

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

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