There and Back Again - Learn iOS 8 App Development, Second Edition (2014)

Learn iOS 8 App Development, Second Edition (2014)

Chapter 12. There and Back Again

Unless your app fits entirely on one screen, your users will need ways of getting around, not unlike the way people navigate cities and towns. We get around via roads, sidewalks, paths, and tracks. You’ll need to lay down the “roads” between the screens of your app so your users can easily get around too. Closely related to organizing your view controllers is determining how they will appear once they are presented. In this chapter, you’ll learn these three essential view controller development skills:

· Connecting view controllers

· Presenting view controllers

· Adapting view controller content

Urban planners have a proven set of solutions (expressways, one-way streets, roundabouts, intersections, overpasses) that they use to provide the best transportation solution for their populace. As an iOS app designer, you also have a rich set of navigation, presentation, and adaption tools that you and your users are already familiar with. The first step is to take stock of the view controllers iOS provides and understand how they work together.

Measure Twice, Cut Once

Like those urban planners, you need a plan. iOS navigation, just like city streets, is difficult to tear up and replace once built. So, begin your design by carefully considering how you want your users to navigate your app. You’ll need to live with your decision or be willing to expend a fair amount of effort to change it in the future.

iOS navigation can also get complicated, which is ironic because the principal player (UIViewController) is a pretty straightforward class. The complication is not in the classes themselves but in how they combine to form larger solutions. I think of them like the elements. It’s not difficult to explain the periodic table—each element has an atomic weight, a number of electrons, and so on. But it’s quite a different matter to consider all of the ways those elements can combine into molecules and interact with one another. In this respect, iOS navigation is kind of like chemistry.

So, sharpen your No. 2 pencil and get ready to take notes because you’re about to learn all about navigation: what it means, how it’s done, the classes involved, and the roles they perform.

What Is Navigation?

Every screen in your app is defined and controlled by a view controller. If your app has three screens, then it has (at least) three view controllers. The base class for all view controllers is UIViewController.

In its simplest terms, navigation is the transition from one view controller to another. Navigation is an activity that view controllers participate in, and view controllers are its currency. Now this is where things begin to get interesting. Navigation is not a class per se, but there are classes that provide specific styles of navigation. While view controllers are the subjects of navigation, some view controllers also provide navigation, some only provide navigation, and some non–view controller classes provide navigation. Are you confused yet? Let’s break it down.

View Controller Roles

View controllers come in two basic varieties. View controllers that contain just view objects are called content view controllers. This is the basic form of view controllers and what you’ve mostly dealt with in this book so far. The entire purpose of navigation is to get a content view controller to appear on the screen so the user can see and interact with it.

The other kind is a container view controller. A container view controller presents other view controllers. It may, or may not, have content of its own. Its primary job is to present, and navigate between, a set of view controllers.

The intriguing part is that both content view controllers and container view controllers are subclasses of UIViewController and are, therefore, all “view controllers.” While a content view controller displays only views, a container view controller can present a parade of content view controllers and container view controllers, the latter of which may present other content view controllers or container view controllers, and so on, down the rabbit hole.

You won’t get confused if you clearly understand the differences and relationship between container view controllers and content view controllers. So, let’s review. Content view controllers display only tangible view objects. The following are examples of content view controllers:

· UITableViewController

· UICollectionViewController

· MPMediaPickerController

· UIViewController

· Every custom subclass of UIViewController you’ve created in this book

Note that UIViewController is on that list. The UIViewController base class is a content view controller. It has all of the basic properties and features needed to display a view, and it does not (implicitly) present any views owned by other view controllers.

Container view controllers present and provide navigation between the views in one or more other view controllers. The following are examples of container view controllers:

· UINavigationController

· UITabBarController

· UIPageViewController

· UIImagePickerController

These view controllers present other view controllers, provide some mechanism for navigating between them, and may decorate the screen with additional view objects that enable that navigation.

So, it’s possible to have a tab bar (container view) controller that contains three other view controllers: a custom (content) view controller, a navigation (container view) controller, and a page (container) view controller. The navigation controller could contain a table (content) view controller and a detail (content) view controller. The page view controller could contain a series of custom (content) view controllers, one for each “page.” Does that sound horribly complex? It’s not. In fact, it’s typical of a medium-sized app design, and it’s exactly the organization of the app you’re about to write. By the end of this chapter, this will seem like child’s play.

Designing Wonderland

The app you’re going to write is based on Lewis Carroll’s famous book, Alice’s Adventures in Wonderland. This seems appropriate given the (sometimes) confounding and convoluted nature of navigation. Here’s a summary of the screens in your app:

· A title page

· The full text of the book

· Some supplementary information about the author

· A list of characters

· Detailed information about each character

The key is to organize the app’s navigation in a way that makes sense, is obvious, is visually appealing, and is easy to use. Start thinking about how you would organize the content of your app while I review the basic styles of navigation available.

Weighing Your Navigation Options

To design your app, you need to know what styles of navigation are available and then what classes and methods provide what you need. Table 12-1 describes the major styles of navigation and the principal classes involved.

Table 12-1. Navigation Styles

Style

Class

Description

Modal

UIViewController

One view controller presents a second view controller. When the second view controller is finished, it disappears, and the first view controller reappears.

Stack or tree

UINavigationController

View controllers operate in a stack. View controllers modally present subview controllers, adding to the stack and navigating deeper into the “tree” of scenes. A navigation bar at the top takes the user to the previous view controller, removing the view controller from the stack and navigating up the “tree” toward the root.

Random

UITabBarController

A tab bar appears at the bottom of the screen. The user can jump immediately to any view controller by tapping one of the buttons in the tab bar.

Sequential

UIPageViewController

The user navigates through a linear sequence of view controllers, moving one view controller at a time forward or backward.

Concurrent

UISplitViewController

This presents two view controllers simultaneously, eliminating the need to navigate between them.

Custom

UIViewController subclass

You decide.

Modal navigation is the simplest and the one you’ve used the most in this book. When DrumDub presented the MPMediaPicker controller and MyStuff presented UIImagePickerController, these view controllers were presented modally. The new view controller took over the interface of the device until its task was complete. When it was done, it communicated its results to your controller (via a delegate message), which dismissed the modal controller and resumed control of the interface. The presented view controller is responsible for implementing an interface that signals when it’s done.

· Use modal navigation when you need to “step out” of the current interface to present relevant details or controls or when you want to perform some task and then immediately return the user to where they were.

The second style of navigation is the stack or tree style, managed by a UINavigationController object. You see this style all over iOS. The Settings app is a particularly obvious example. The signature of the navigation controller is its navigation bar that appears at the top of the screen (see examples in Chapter 5). It shows users where they are and has a button to allow them to return to where they were. When a content view controller (modally) presents a new view controller, the navigation controller adds it to the stack of view controllers the user can step back through. This is called pushing a view controller. When used in the context of a navigation controller, a view doesn’t have to provide a means for returning to the presenting view controller because the back button in the navigation bar provides that action.

You can (within strict limits) customize the navigation bar, adding your own titles, buttons, or even controls. The navigation controller can also add a toolbar at the bottom of the display, which you can populate with buttons and indicators. Both of these elements are owned and managed by the navigation controller.

· Use a navigation controller when there are several layers of modal views in order to keep the user informed about where they are, tell them where they came from, and provide a consistent method of returning.

The UITabBarController manages a set of view controllers the user can maneuver through arbitrarily. Each view controller is represented by a button in a tab bar at the bottom of the screen. Tap a button and that view controller appears. The iOS Clock app is a perfect example.

· Use a tab bar to allow quick and direct access to functionally different areas of your app.

The UIPageViewController is equally easy to understand. It presents a sequence of view controllers one at a time. The user navigates to the next or previous view controller in the sequence by tapping or swiping on the screen, as if leafing through the pages of a book. Apple’s Weather app is the iconic example of a page view controller in action.

· Use a page view controller, as an alternative to UIScrollView, when you have more information than can be presented on a single screen or an unbounded set of functionally similar screens that differ only in content.

The UISplitViewController is a navigation controller that eliminates the need for navigation. This special container view controller simultaneously presents two view controllers side by side. On compact devices with limited screen space (like an iPhone), the split view controller will naturally collapse to show only one view at a time, dynamically replacing the two views with a navigation view controller interface. You’ve already seen the split view controller in action in your MyStuff app.

· Use a split view controller to present more content on a single screen (space permitting), reducing the need for navigation.

Finally, it’s possible to create your own style of navigation. You can subclass UIViewController and create a container view controller with whatever new kind of navigation you invent. I would, however, caution you about doing this. The existing navigation styles are successful largely because they are familiar to users. If you start designing spiral sidewalks or streets that go backward on Tuesdays, you might be creating a navigation nightmare rather than navigation nirvana.

Wonderland Navigation

Figure 12-1 shows the design for the Wonderland app. The main screen—called the initial view controller—will be a tab view with three tabs. The first tab contains a content view with the book’s title and an info button that (modally) presents some details about the author.

image

Figure 12-1. Wonderland app design

The middle tab lists characters in the book in a table view. Tapping a row transitions to a detail view with more information. This interface is under the control of a navigation controller, so a navigation bar provides a way back to the list.

The book appears in the last tab, a page view controller, where users can swipe and tap their way through the text.

Creating Wonderland

Launch Xcode and create a new project. (I’m sure you saw that one coming.) This time, create the project based on the Tabbed Application template, as shown in Figure 12-2. Name the app Wonderland, set the language to Swift, and make it Universal.

image

Figure 12-2. Project template for Wonderland

The initial view controller presented by your app will be a tab bar controller; the Tabbed Application template creates a project whose initial view controller is a tab bar controller. By cleverly choosing the Tabbed Application template, your first step is already done. You’ve created aUITabBarController object and installed it as the app’s initial view controller.

Tip The initial view controller is the view controller presented when your app starts. You can create it programmatically in the startup code of your application delegate object, or you can let iOS present it for you. For the latter to happen, you need to set its Is Initial View Controller property to true. You can set this in Interface Builder by checking the Is Initial View Controller option using the attributes inspector or by dragging around the initial view controller arrow (shown on the left side of the tab bar controller object in Figure 12-3) and attaching it to the view controller of your choice.

image

Figure 12-3. Starting tab bar configuration

Remember that a tab bar controller is a container view controller. It doesn’t display (much) content of its own. Select the Main.storyboard Interface Builder file, as shown in Figure 12-3. The big blank area in the middle of the tab view controller is going to be filled in with the contents of some other view controller. The storyboard shows that your tab bar controller came preconfigured with two content view controllers, FirstViewController and SecondViewController.

To use a tab bar, you must provide a pair of objects for each tab: a view controller to display and a tab bar item (UITabBarItem) that configures that tab’s button at the bottom of the screen. Each tab bar item defines a title and an icon. Icons smell suspiciously like resource files, so start there.

Note A container view controller can show interface elements—like the tab bar—that is not part of or is outside of the content of the presented view controller. Everything inside a view controller’s root view object is called its content. Everything outside is called the chrome.

Adding Wonderland’s Resources

I’m going to have you cheat (a little bit) and add all of the resources for this project at once. This will save you (and me) from repeating these steps for each interface you’re going to develop in this chapter. Just add them all now; I’ll explain them as you need them.

In earlier projects, I had you add individual resource files to the main top-level group (the folder icon) in your project navigator or to the Images.xcasset asset catalog. There are a sufficient number of resource files in this project that I’m going to have you create subgroups so they don’t become unwieldy. There are three ways you can organize source files in your project.

· Create a subgroup and then create or add new files to that group

· Import folders of source files and let Xcode create groups for each folder

· Wait until you have too many files cluttering up your navigator and then decide to organize them

To use the first or third method, create a new subgroup using the File image New Group command (also available by Control+clicking or right-clicking in the project navigator). Name the new group and then import resource files, create new source files, or drag existing files into it. Developers tend to organize their groups either by file type (all of the data files in one group, with class source files in another) or by functional unit (all of the source and resources files for a table in a single group). It’s a matter of style and personal preference.

Tip If you decide to use the third method, which is my personal favorite, make use of the File image New Group from Selection command. Select the files you want to organize into a group and choose New Group from Selection. It creates a new subgroup and moves all of those items into it in one step.

The second method is handy when you’re importing a large number of resource files at once. Find the Learn iOS Development Projects image Ch 12 image Wonderland (Resources) folder. These resource files have been organized into subfolders: Data Resources,Character Images, Info Images, and Tab Images. Instead of dragging the individual files into the project navigator, you’ll drag the folders into your project, importing all of the resource files at once. Begin with the data (nonimage) files in the Data Resources folder. Drag that folder and drop it into the Wonderland group, as shown in Figure 12-4.

image

Figure 12-4. Adding a folder of resource files

When the import dialog appears, make sure the Create groups option is selected, as shown on the left in Figure 12-5. This will turn each folder’s worth of resource files into a group, as shown on the right in Figure 12-5.

image

Figure 12-5. Copying and creating groups for new resource folders

To do something similar for your images, choose the Images.xcassets asset catalog item and drag all three folders of images (Character Images, Info Images, and Tab Images) into the catalog’s group column, as shown on the left in Figure 12-5. This will automatically create three groups of images, as shown on the right of Figure 12-6.

image

Figure 12-6. Importing groups of image files

In the interests of neatness, let’s discard some detritus you don’t need. Select the first and second image sets in the asset catalog. While holding down the Command key, press the Delete key (or choose Edit image Delete) to remove these items from your project.

Note You’ll also find some app icons in the Wonderland (Icons) folder. Drop those into the AppIcon image set, if you like.

Configuring a Tab Bar Item

Now that you have all of your resources, configure the tab bar for the first tab. Each tab button in the tab bar is configured via a UITabBarItem object associated with its view controller. When a view controller is added to a tab bar controller, Interface Builder automatically creates this object in the scene that defines that view controller. Select the Main.storyboard file. Find and expand the first view controller group, as shown on the left in Figure 12-7. Select the tab bar item object, use the attributes inspector to change its title to Welcome, and set its image to tab-info, as shown on the right in Figure 12-7. When you change the name of the bar item, the name of the scene will change to Welcome Scene.

image

Figure 12-7. Configuring the first tab bar item

Note The image for the tab bar button is not displayed “as is.” The image you supply is used like a stencil, creating a silhouette from the opaque pixels of the image. So, don’t bother designing your tab bar button images with pretty colors; only the transparency matters.

You’ll repeat these steps for each content view you add to the tab bar. Now move on to the content for this first tab.

The First Content View Controller

The first tab presents a simple content view controller, based on UIViewController. The Xcode template has already created a custom view controller (FirstViewController) and attached it as the contents of the first tab. This is almost exactly what you want, so gut it and make it your own.

Select the Main.storyboard file. Double-click the first view controller (upper right) in the canvas to make it the focus. The view already contains some label and text view objects. Select these and delete them.

Using the object library, add two labels and one image view object. Using the attributes and size inspectors, set their properties as follows:

1. First label

a. Text: Alice’s Adventures in Wonderland

b. Font: System 16.0

2. Second label

a. Text: by Lewis Carroll

b. Font: System 13.0

3. Image view

a. Image: info-alice.png

b. Mode: Aspect Fit

Tip After changing the text, font, or image of an object, if its content no longer exactly fits its dimensions, select it and use the Editor image Size to Fit Contents command. It will resize the object so it’s exactly the same size as the image or text it contains, also called its intrinsic size.

Arrange the views so they look something like those in Figure 12-8.

image

Figure 12-8. Creating the first view controller interface

Next add the constraints. Select all three view objects and use the alignment constraints control to add center horizontally constraints. Then select just the image view and center it vertically. With the image view still selected, use the pin constraints control to add vertical spacing constraints above (30 pixels) and below (8 pixels) and pin both the height and width to 300 pixels, as shown in Figure 12-8. The layout for your first tab view is finished. (Well, you’re almost finished. Later in this chapter you’ll return and add a button).

Choose a simulator and run your app. Your first view controller appears (as shown on the left in Figure 12-9), embedded in the tab view controller. You can switch between the two view controllers using the buttons at the bottom of the screen, as shown on the right in Figure 12-9.

image

Figure 12-9. Two view controllers in a tab view controller

To review, you’ve designed a content view controller (your welcome screen) and added it to a container view controller (the tab view). Now get crazy and add a container view controller to a container view controller.

Creating a Navigable Table View

The second tab of your Wonderland app presents a list of characters in a table view. Tapping a row navigates to a screen with some character details. Does this sound familiar? It should. You already built this app in Chapter 5. Well, you get to build it again. But this time, the focus is going to be on navigation.

You know from Chapter 5 that you’re going to need the following:

· A navigation view controller

· A custom subclass of UITableViewController (for the table view)

· A data model

· A table view delegate object

· A data source object

· A table view cell object

· A custom subclass of UIViewController (for the detail view)

· View objects to display the detail view

Start with the navigation view controller. A navigation view controller is a container view controller. The view it initially displays is its root view controller. This view is its home base, which is the view that all navigation starts from and eventually returns to. To have the second tab of the Wonderland app present a navigable table view, you need to install a navigation controller as the second view controller in the tab bar and then install a table view controller as the root view controller of the navigation controller. This is easier than it sounds.

Start by clearing some room. Select the Main.storyboard file. A SecondViewController already occupies the second tab. You don’t need it. Select the second view controller scene in the Interface Builder canvas and delete it. Then select the SecondViewController.swiftfile and delete it too.

From the object library, drag in a navigation controller and place it underneath the first view controller, as shown in Figure 12-10. A new navigation controller comes pre-installed with a table view controller, which is exactly what you want. (See, I told you this wouldn’t be too hard.) You’ll also need a detail view, so drop in another view controller object next to the table view, also shown in Figure 12-10.

image

Figure 12-10. Adding a navigation controller, table view controller, and view controller

To add the navigation controller to the tab bar, Control+click or right-click the tab bar controller and drag to the navigation controller, as shown in Figure 12-11. When the pop-up appears, find the Relationship Segue category and choose view controllers. This special connection adds the view controller to the collection of controllers that the container view controller manages. A second tab appears in the tab view, and a companion tab bar item object is added to the navigation controller’s scene.

image

Figure 12-11. Making the navigation controller the second tab

Expand the Navigation Controller Scene group in the outline and select the tab bar item. Use the attributes inspector to set the title to Characters and the image to tab-chars.

You’ve now added a navigable view controller to your tab bar (container) view controller. It’s the second tab. It has a title and icon. Tapping it will present the table (content) view controller inside the navigation (container) view controller. It sounds complicated, but the storyboard makes the organization easy to follow.

Breathing Data Into Your Table View

You can run your app right now, tap the Characters tab, and marvel at the raging emptiness of your table view. You know, from Chapter 5, that without a data source and some data, your table view has nothing to display. Let’s tackle that now.

You’re going to need a custom subclass of UITableViewController, so create one. You also know that you’re going to need a custom subclass of UIViewController for your detail view. While you’re here, you might as well create that too. Add (or drag in) a new Swift file. Name it CharacterTableViewController and edit it so it looks like this:

import UIKit

class CharacterTableViewController: UITableViewController {
}

Similarly, add (or drag in) a new Swift file, name it CharacterDetailViewController, and edit it so it looks like this:

import UIKit

class CharacterDetailViewController: UIViewController {
@IBOutlet var nameLabel: UILabel!
@IBOutlet var imageView: UIImageView!
@IBOutlet var descriptionView: UITextView!
}

Reviewing the list of things you need to do to get the table view working, you now have a navigation controller and custom subclasses of the table and view controllers. But the objects in the interface aren’t your custom subclasses yet. Select the Main.storyboard file, select the table view controller, and use the identity inspector to change its class to CharacterTableViewController. Do the same for the detail view controller, making its class CharacterDetailViewController.

Creating the Detail View

Since you’re already in the character detail view controller, go ahead and create its interface. Use the object library to add a label, an image view, and a text view to the character detail view controller. Position the label view at the top, the image in the middle, and the text view below it, something like the arrangement shown in Figure 12-12. You’re not going to add any constraints yet.

image

Figure 12-12. Initial character detail layout

Select the view controller object and switch to the outlets inspector. Connect the nameLabel, imageView, and descriptionView outlets to their respective objects in the interface, also shown in Figure 12-12.

Adding the Data Model

What’s left? You still have to create a data model and provide the table view with a data source and table view cell object. Start with the data model.

I have a surprise for you. I created a data model for you. Isn’t that nice of me? The character detail information is stored in an object array. Each element of the array contains a dictionary. Each dictionary has the name of the character, the filename of its image, and a brief description. All of this information is stored in the Characters.nsarray file, one of the resource files you added earlier in this chapter.

Note The Characters.nsarray file is a serialized property list XML file. You can view it in Xcode or almost any plain-text editor, if you want to look at it. I created the file by writing an OS X command-line program that creates all of the dictionaries, assembles them into an array, and writes (serializes) the array to a file. The project that does this is in the Learn iOS Development Projects image Ch 12 image CharacterMaker folder. Property lists and serialization are explained in Chapter 18.

Add the data model to your table view controller by creating a property to hold the array. Add the following properties to your CharacterTableViewController.swift file:

var tableData: [[String: String]] {
if tableData_Lazy == nil {
if let url = NSBundle.mainBundle().URLForResource( "Characters", image
withExtension: "nsarray") {
tableData_Lazy = NSArray(contentsOfURL: url) as? [[String: String]]
}
assert(tableData_Lazy != nil, "Characters.nsarray did not load")
}
return tableData_Lazy!
}
private var tableData_Lazy: [[String: String]]?

You saw this sort of thing in Chapter 9. This declares a read-only property named tableData that returns an array of dictionaries, each of which maps a string key to a string value ([[String: String]]). This array is lazily read from the Character.nsarray file the first time it’s requested, and it’s stored in the private tableData_Lazy property for future use.

USING ASSERTS

Note the use of the assert(_:,_:) statement in the tableData property getter. Assert statements have two arguments: a Boolean condition and a string description. They’re used during development to express any strong assumptions you’re making. The first argument expresses something in your program that you always assume is true, and the message describes what might be wrong if it isn’t. In most apps, there are lots of places where you assume something about values or relationships. You might assume that a size is always nonzero, a date is always in the future, or an outlet has always been set.

In this case, tableData should always return a value. This assumes that the Characters.nsarray file is a permanent resource of your app and can always be loaded. This really shouldn’t be an issue. There’s no point in writing code to consider the possibility that theCharacters.nsarray file isn’t there or can’t be loaded. But if it’s not true and tableData returns nil, your app is going to be a world of hurt. The assert(_:,_:) function catches that condition, terminates your app, and sends the message (“Characters.nsarray did not load”) to your Xcode debug console. This helps you find these kinds of mistakes during development; maybe you forgot to include the Characters.nsarray resource file in the app’s target or you accidentally renamed the file.

Sure, you could leave the assert statement out and let tableData return nil, but some random code in another part of your program will try to use that nil value and crash your app. You’ll then spend five minutes trying to figure out what caused a memory segment fault. With the assert statement, you’ll catch the problem immediately and know exactly what’s wrong, shortening your debugging efforts.

Assert functions do not execute in production code. They test their conditional only when you build your app for debugging. When you compile your app for distribution, all of the assert statements turn off, almost as if they weren’t there.

The keys for the dictionaries need to be defined as constants. Before the class CharactersTableViewController declaration, add these three global constants:

let nameKey = "name"
let imageKey = "image"
let descriptionKey = "description"

Declaring them outside the class definition makes them global properties, easily accessible from other classes (the CharacterDetailViewController will need these too). You now have a data model!

Implementing Your Data Source

Now you need to feed the table view this information via its data source object. While still in the CharacterTableViewController.swift file, add a tableView(_:,numberOfRowsInSection:) function.

override func tableView(tableView: UITableView, image
numberOfRowsInSection section: Int) -> Int {
return tableData.count
}

This function provides the table view with the number of rows in the list, which is the number of objects in the array.

Note You might have noticed that you haven’t connected the table view’s delegate or dataSource outlet to your table view controller. That’s because your controller is a subclass of UITableViewController, which is specifically designed to manage a table view. If youdo not connect the delegate or dataSource outlets yourself, the controller makes itself both the delegate and the data source for the table automatically. Isn’t that convenient?

Defining a Table View Cell Object

The last piece of the puzzle is to supply the table view with a table cell object for each row—the table view’s rubber stamp. Add this tableView(_:,cellForRowAtIndexPath:) function immediately after your tableView(_:,numberOfRowsInSection:) function:

override func tableView(tableView: UITableView, image
cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cellID = "Cell"
let cell = tableView.dequeueReusableCellWithIdentifier( cellID, image
forIndexPath: indexPath) as UITableViewCell
let characterInfo = tableData[indexPath.row]
cell.textLabel?.text = characterInfo[nameKey]
return cell
}

This code should look familiar—unless you skipped Chapter 5. The cell’s appearance is defined by the cell prototype in the storyboard, which you still need to define. Switch to the Main.storyboard file and locate the table view controller.

At the top of the table view you’ll see an area labeled Prototype Cells, as shown in Figure 12-13. Select the first, blank cell and use the attributes inspector to change its style to Basic, set its identifier to Cell, and change its accessory to Disclosure Indicator (see Figure 12-13).

image

Figure 12-13. Defining a table cell

Now when your tableView(_:,cellForRowAtIndexPath:) function asks for the Cell table cell object, it’s already there. Your code just configures the text of the cell and it’s done.

Your table view now has data. Run the app in the simulator and tap the second tab, and this time your table is populated with the names from the data model, as shown in Figure 12-14.

image

Figure 12-14. Working table view

Pushing the Detail View Controller

Tapping a row in the table, however, doesn’t do much (on the right in Figure 12-14). That’s because you haven’t defined the action that presents the detail view.

You’re going to add a segue from the table cell view to your detail view. Control+click or right-click on the prototype cell object and drag to the character detail view controller (also shown in Figure 12-15). When you release the mouse, choose the show option from the Selection Segue group. This configures all rows that use this cell object to “push” the character details view controller onto the navigation controller’s stack, presenting it as the active view controller.

image

Figure 12-15. Connecting a segue to the table cell view

Just as you did in Chapter 5, you need some code to prepare the detail view based on the row the user tapped. For that to work, you’ll need to know when the segue is triggered. Select the newly created segue and use the attributes inspector to change its identifier to detail, as shown in Figure 12-16.

image

Figure 12-16. Assigning a segue identifier

Return to the CharacterTableViewController.swift file and add this function:

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
if segue.identifier == "detail" {
let detailsController = segue.destinationViewController image
as CharacterDetailViewController
if let selectedPath = tableView?.indexPathForSelectedRow() {
detailsController.characterInfo = tableData[selectedPath.row]
}
}
}

This function is called whenever a segue is triggered in this view controller. The segue object contains information about the view controllers involved (both coming and going). Use it to get the details view controller object that the storyboard just created and loaded. You then use thetableView object to get the row number of the currently selected row—the one the user is tapping—and use that to get the character details from the data model and configure the new view controller (by setting characterInfo).

There are still a few loose ends. The detail view controller doesn’t have a characterInfo property yet! Switch to your CharacterDetailViewController.swift file and add the following code:

var characterInfo = [String: String]()

override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
nameLabel?.text = characterInfo[nameKey]
imageView?.image = UIImage(named: characterInfo[imageKey]!)
descriptionView?.text = characterInfo[descriptionKey]
}

Your details view controller now has a characterInfo property. When the view controller is about to appear on the screen, this code will populate the view objects with details in characterInfo. Run the app in the simulator to try it, as shown in Figure 12-17.

image

Figure 12-17. Working character table

While the information is there, you’ll notice that the views in the detail view controller aren’t arranged nicely, nor do they adjust their size. If you’re running this on an iPhone, it’s going to look even worse. Don’t worry. You’ll fix that in the “Being Adaptable” section later in this chapter.

Your app is now two-thirds finished. In this section, you created a table view and a detail view, nested inside a navigation view controller, nested inside a tab view controller.

By now you should be getting pretty comfortable with content and container view controllers, connecting them and creating segues to define your app’s navigation. The final section of this chapter is going to show you how to use a page view controller.

Creating a Page View Controller

You have arrived at the third, and final, tab of your Wonderland app. This tab will display the text of the book, one page at a time. This tab uses a page view controller (UIPageViewController) object. It’s a container view controller that manages a (potentially huge) collection of content view controllers. Each “page” consists of one or two content view controllers. The page view controller provides gesture recognizers that perform and animate the navigation between pages in the collection.

Adding a page view controller to your design is simple enough. Getting it to work is another matter. Page view controllers are typically code intensive, and this app is no exception. To make the situation even more exciting, you have to get the bulk of your code working before the page view will do anything at all. So, settle in, this is going to be a long trip.

You’re going to need a number of new classes, so create them all now. Use the New File command (or drag in Swift file template objects) to create new Swift files in your project’s Wonderland group. As you create each new file, write a skeleton class definition. This will let Xcode know what classes you’re going to write, what their superclasses are, and a few key outlets. This will let you design your interfaces before writing the bulk of the code.

Begin by creating a BookViewController.swift file. This class will be your custom page view controller.

import UIKit

class BookViewController: UIPageViewController {
}

Create a BookDataSource.swift file. This will become your page view controller’s data source object.

import UIKit

class BookDataSource: NSObject, UIPageViewControllerDataSource {
}

Add a OnePageViewController.swift file to your project. This will be the view controller for each individual page of the book, and it needs some outlets to its views.

import UIKit

class OnePageViewController: UIViewController {
@IBOutlet var textView: OnePageView!
@IBOutlet var pageLabel: UILabel!
}

Finally, add a OnePageView.swift file. This is a custom UIView class that draws the text of one page. This one is so short, you might as well write the whole thing now.

import UIKit

class OnePageView: UIView {
var text: NSString = "" { didSet { setNeedsDisplay() } }
var fontAttrs: [String: AnyObject]! = nil

override func drawRect(rect: CGRect) {
super.drawRect(rect)
text.drawInRect(bounds, withAttributes: fontAttrs)
}
}

You should be able to decipher this, having read Chapter 11. When it’s time to draw itself, a OnePageView object fills its view with the background color (super.drawRect(rect) does that for you) and then draws its text using the attributes stored in its fontAttrs property.

I’ve omitted the code for the last class you’ll need. It’s rather long and is not the focus of this chapter. Locate the source code you downloaded. In the Learn iOS Development Projects image Ch 12 image Wonderland folder, find the Paginator.swift file and drag it into the Wonderland group of your project—remembering to check the Copy option when you import it.

The Paginator object is conceptually simple. It has three settable properties: the text of the entire book (as a single string), a font, and a view size. The object splits up the text into pages, each one of which will exactly fill the given view size using that font. If you’re interested in the details, read the comments in the code.

Note This is hardly the most sophisticated way of implementing a paginator, but it’s sufficient for this app.

You’re now ready to add your view controllers and design the interface.

Adding the Page View Controllers

It’s not all code. Use Interface Builder to create the two view controllers. Select the Main.storyboard file. Drag a new Page View Controller from the object library and add it to your design. Also add a new View Controller object, as shown in Figure 12-18. Arrange them below the other scenes.

image

Figure 12-18. Adding the page view controller and single page view controller

Add the page view controller to the tab bar by Control+dragging or right-click-dragging from the tab bar controller to the new page view controller, as shown in Figure 12-19, just as you did earlier for the navigation view controller. Select the view controllers relationship.

image

Figure 12-19. Adding the page view controller to the tab bar

As you did with the other tabs, select the tab bar item in the page view controller’s scene. Use the attributes inspector to set its title to Book and its tab icon to tab-book.

Now configure the page view controller itself. Select the page view controller object and use the identity inspector to change its class to BookViewController. Switch to the attributes inspector and double-check that the following properties are set:

· Navigation: Horizontal

· Transition Style: Page Curl

· Spine Location: Min

These settings define a “book-like” interface where the user moves horizontally through a collection of view controllers, one per page. (If you set the Spine location to the Mid, you’d get two view controllers per page.) A transition between controllers emulates the turning of a paper page.

Designing a Prototype Page

Now move over to the plain view controller you just added. Use the identity inspector to change its class to OnePageViewController. Also change its Storyboard ID to OnePage. This last step is important. This controller won’t be connected in Interface Builder; you’re going to create instances of it programmatically. To do that, you need a way to refer to it, and you’ll use its storyboard ID to do that.

With the preliminaries out of the way, create the interface for the one page view controller. From the object library, add three view objects as follows:

1. Label

a. Font: System 15.0

b. Text: Alice’s Adventures in Wonderland

c. Position at top center

2. Label

a. Font: System 11.0

b. Position at bottom center

3. View

a. Center between the two labels to fill the available space.

b. Use the identity inspector to change its class to OnePageView.

4. Constraints

a. Select both label objects and add center horizontally constraints.

b. Select the top label and add a vertical constraint to the top layout guide (standard distance).

c. Select the bottom label and add a vertical constraint to the bottom layout guide (standard distance).

d. Select the image view and add top, leading, trailing, and bottom constraints, all set to 20 pixels, as shown in Figure 12-20.

image

Figure 12-20. One page view controller layout

Select the one page view controller object and use the connections inspector to connect its two outlets to the OnePageView object and the label at the bottom (which will display the page number), as shown in Figure 12-21.

image

Figure 12-21. Connecting the page view objects

Coding the One Page View Controller

You’ve now done just about all you can do in Interface Builder. It’s time to roll up your sleeves and start coding. This time, start from the “tail” end of this design and work back up to the page view controller, filling in the details as you go. The last object in the chain is OnePageView, the custom view that displays the text of a page. You’ve already written that, and it was pretty simple. (If you’ve already forgotten, sneak back to the “Creating a Page View Controller” section).

Let’s move on to the view controller. You’ve already defined the class, added two outlets (textView and pageLabel), and connected those outlets to their view objects. Now add a couple of properties and a loadPageContent() function.

var pageNumber = 1
var paginator: Paginator? = nil

func loadPageContent() {
if let tv = textView {
if let pager = paginator {
pager.viewSize = tv.bounds.size
if !pager.pageAvailable(pageNumber) {
pageNumber = pager.lastKnownPage
}
tv.fontAttrs = pager.fontAttrs
tv.text = pager.textForPage(pageNumber)
}
}
pageLabel?.text = "Page \(pageNumber)"
}

The pageNumber determines the page number of the book this view controller displays. It will be set by the data source when the controller is created. The paginator property connects this view controller to the book’s content. The loadPageContent() function starts by configuringpaginator with the actual size of the text view. It then simply asks the paginator for the text for this page and the font attributes that should be used to draw it. It transfers those values to the textView (OnePageView) object that will do that actual drawing. TheOnePageViewController is really just “glue” between its OnePageView and the Paginator.

Finally, the pageLabel view is set to display the page number of the book.

So, when does loadPageContent() get called? Under most circumstances, this kind of first-time view setup code would be invoked from your viewDidLoad() function. But loadPageContent() needs to be called whenever the size of text view changes, and that can happen at any time, most notably when the user changes the display orientation. Solve that by overriding the viewDidLayoutSubviews() function and calling loadPageContent() whenever the controller’s view layout is adjusted.

override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
loadPageContent()
}

I talk about viewDidLayoutSubviews() and the other adaption functions later in this chapter.

Coding the Page View Data Source

You finally get to the heart of the page view controller: the page view data source. A page view controller data source must conform to the UIPageViewControllerDataSource protocol and implement these two required functions:

pageViewController(_:,viewControllerBeforeViewController:) -> UIViewController
pageViewController(_:,viewControllerAfterViewController:) -> UIViewController

The page view starts with an initial view controller to display. When the user “flips” the page to the right or left, the page view controller sends the data source object one of these messages, depending on the direction of the page turn. The data source, using the current view controller as a reference, retrieves or creates the view controller that will display the next (or previous) page. If there is no page, it returns nil.

Your data source must implement these methods. It will also need a property that refers to the single paginator object used by all of the individual view controllers and a function to create the view controller for an arbitrary page. Your BookDataSource.swift file, therefore, should start like this:

let paginator = Paginator(font: UIFont(name: "Times New Roman", size: 18.0))

func load(# page: Int, pageViewController: UIPageViewController) image
-> OnePageViewController? {
if page < 1 || !paginator.pageAvailable(page) {
return nil;
}
let controller = pageViewController.storyboard?.instantiateViewController image
WithIdentifier("OnePage") as OnePageViewController
controller.paginator = paginator
controller.pageNumber = page
return controller
}

The paginator property is initialized with a Paginator object that will use the Times New Roman font at 18 points to display the text.

The load(page:,pageViewController:) function is where all of the works gets done. You call this function with a page number, and it returns a view controller that will display that page. If the requested page doesn’t exist, it returns nil. It asks the storyboard object (that it obtains via the page view controller) to create the controller and views contained in the scene with the identifier OnePage. This is done because segues and actions aren’t used to navigate between view controllers in a page view. It’s up to the data source to create them when requested.

Note Remember, earlier you assigned the view controller scene in the storyboard an identifier of OnePage. This is why. If you need to programmatically load a view controller and its view objects from a storyboard scene, theinstantiateViewControllerWithIdentifier(_:) function is your ticket.

Once it has a new one page view controller object, it connects it to the paginator and sets the page number it should display.

All that’s left to do is to implement the two required data source protocol methods. These also go in your BookDataSource class.

func pageViewController(bookViewController: UIPageViewController, image
viewControllerAfterViewController viewController: UIViewController) image
-> UIViewController? {
if let pageController = viewController as? OnePageViewController {
let pageAfter = pageController.pageNumber + 1
return load(page: pageAfter, pageViewController: bookViewController)
}
return nil
}

func pageViewController(bookViewController: UIPageViewController, image
viewControllerBeforeViewController viewController: UIViewController) image
-> UIViewController? {
if let pageController = viewController as? OnePageViewController {
let pageBefore = pageController.pageNumber - 1
return load(page: pageBefore, pageViewController: bookViewController)
}
return nil
}

Since each one page view controller stores the page number it displays, all these two functions have to do is request the page after or before the current one.

Initializing a Page View Controller

Your book implementation is almost complete. The only thing left to do is perform some initial setup of the page view controller and data model when the page view controller is created. Switch to the BookViewController.swift file. Begin by creating a property to store the data source object.

let bookSource = BookDataSource()

Note Why it’s necessary to create a bookSource instance variable has to do with a quirk of the automatic reference counting (ARC) memory management system. Read the comments in the finished project and Chapter 20 for an explanation.

Now write a viewDidLoad() function to initialize the page view controller when it’s created.

override func viewDidLoad() {
super.viewDidLoad()
if let textURL = NSBundle.mainBundle().URLForResource( "Alice",
withExtension: "txt") {
bookSource.paginator.bookText = NSString(contentsOfURL: textURL,
encoding: NSUTF8StringEncoding, error: nil)
}

This first block of code reads in the text of the book, stored in the Alice.txt file. This was one of the resource files you added at the beginning. The file is a UTF-8 encoded text file with each line separated by a single carriage return (U+000d) character. This format is what the paginator expects. The entire text is read into a single string and stored in the paginator, which will use it to assign text to individual pages.

dataSource = bookSource

The next statement makes the bookSource object the data source for the page view controller.

let firstPage = bookSource.load(page: 1, pageViewController: self)!
setViewControllers( [firstPage],
direction: .Forward,
animated: false,
completion: nil)
}

This last statement is probably the most important. It creates the initial view controller that the page view controller will present by explicitly creating the controller for page 1. This must be done programmatically before the page view controller appears.

Caution The initial view controller for a page view controller is an array. The number of view controllers must exactly match the number of view controllers the page view controller presents at one time. If you configure the page view controller with a spine location of .Mid, you must provide two initial view controllers: one for the left page and a second one for the right page.

That was a lot of code, but you’re done! Run your app and test the third tab, as shown in Figure 12-22.

image

Figure 12-22. Working page view interface

Congratulations, you’ve created a truly complex app. You were aided, in part, by storyboards that allowed you to map out and define much of your app’s navigation in a single file. But you also learned how to load storyboard scenes programmatically when needed.

I encourage you to take a moment and review the scenes in your storyboard file and the classes you created to support them. Once you’re comfortable that you understand the organization of your view controllers, how they work together, and the roles of the individual classes you created, you can consider yourself a first-class iOS navigation engineer.

Now it’s time to master the other two essential navigation skills: presenting view controllers and adapting view controllers.

Presenting View Controllers

Presenting a view controller makes the content of that view controller visible and allows the user to interact with it. You’ve been doing this since Chapter 2. Often, your view controllers are implicitly presented. That’s the case with your initial view controller and the view controllers in container view controllers, like many of the ones you’ve created in this chapter. You didn’t write any code to present the three view controllers in the tab view; you simply added them to the tab view controller and let it present them.

You explicitly present a view controller using the presentViewController(_:,animated:,completion:) function. This is almost always a modal presentation. That is, the presented view controller consumes the entire display (or overlays the existing interface) and takes over the user interaction until it is dismissed. At that time, it disappears, and the previous view controller becomes visible and active again.

You’ve been explicitly presenting modal view controllers since Chapter 3. You’ve presented alerts, the media picker, the photo library picker, and the camera interface. You’ve presented view controllers full-screen and in popovers. You’ve written code to collect the results of the presented controller and dismissed them when they were done.

So, you might be wondering what else there is to know about presenting view controllers. There’s quite a lot actually. In this section you’re going to learn about the different objects involved in presenting a view controller, the different presentation styles available, the default presentation behavior, the view controller transitions, and how you can override and customize presentations. Let’s start by introducing the players.

Presentation Controllers

When presenting a view controller, there are a few key objects, as shown in Figure 12-23.

image

Figure 12-23. View controller presentation objects

The presenting view controller is the (currently active) view controller that wants to present a new view controller. The presented view controller is the new view controller that will be presented. When a presenting view controller wants to present another view controller, it callspresentViewController(_:,animated:,completion:).

The presentation controller is the object that manages the presentation. It’s created at the same time as the view controller and exists until the view controller is dismissed. The presentation controller (UIPresentationController) determines how the new view controller appears on the screen. It might replace the entire screen with the new view controller, or it might put it inside a popover. This is a combination of the style the view controller would prefer to be presented in (its modelPresentationStyle), the style the presentation controller thinks it should be presented in, and the style the presentation controller delegate wants it to be presented in. There are a number of points in this decision chain where you can affect how your view controller is presented, which you’ll explore in the next few sections.

Another object of interest is the transition coordinator (UIViewControllerTransitionCoordinator). This object is responsible for the transition between view controllers and the resizing of their content. When you tap a button and a new view controller slides up from the bottom, it’s a transition coordinator directing that animation. And when you rotate the device, a transition coordinator orchestrates the smooth metamorphosis of the layout. Transition coordinators are created automatically at the beginning of a transition and disappear once it’s over. You’ll see how to use the transition coordinator later in this chapter.

To play around with view controller presentation, let’s add a simple button that modally presents a custom view controller to your Wonderland app user. You can then explore different ways in which you can affect its presentation.

Presenting a Modal View Controller

Return to the Main.storyboard file and drag a button into the Welcome view, just to the right of the “by Lewis Carroll” text. Set its type to Info Dark, delete its title, and add a left horizontal constraint, as shown in Figure 12-24. Select both the button and the label and add an align vertical centers constraint.

image

Figure 12-24. Adding the author info button

Now add a new view controller to your storyboard. Drop it into the canvas right next to the Welcome Scene. Select the new view controller object, switch to the size inspector, and change its Simulated Size to Freeform. Give it a width of 250 and a height of 340 (see Figure 12-25).

image

Figure 12-25. Creating a free-form view controller

Notice that the attribute is named Simulated Size. Interface Builder doesn’t actually know how big your view controller will be when it’s presented, but it will allow you to design it with a nontypical size.

Now you need to encourage it to be that size. Switch to the attributes inspector, find the Content Size properties, check the Use Preferred Explicit Size attribute, and set the width to 250 and the height to 340. This sets the view controller’s preferredContentSize property. When a view controller is presented in such a way that its size can be variable (as in a popover as opposed to always being the size of the screen), this property is the size it would like to be. The presentation controller will try to accommodate it, but it isn’t required to.

Add an image view and a label object to the new view controller, as follows:

1. Image view

a. Set Image to info-charles.

b. Set Mode to Aspect Fit.

c. Add a vertical constraint (20 pixels) to the top layout guide.

d. Add a center horizontally in superview constraint.

e. Pin the height to 244 pixels and the width to 164 pixels.

2. Label view

a. Set Text to “Lewis Carroll” and “a.k.a. Charles Lutwidge Dodgson” and “27 January 1832 – 14 January 1898” on three lines. (Hold the Option key down when pressing the Return key to create line breaks in the text.)

b. Set Font to System 12.0.

c. Set the alignment to centered.

d. Set Lines to 3.

e. Add top (8), leading (0), and trailing (0) constraints.

Your finished layout should look like the one in Figure 12-26.

image

Figure 12-26. Finished author info layout

If you wanted to present this view controller modally and full-screen, you could do something like this:

1. Add an action to your FirstViewController (something like @IBAction func showInfo(_: AnyObject!)).

2. Connect the button to the new action.

3. Assign a storyboard ID to the new view controller.

4. In your showInfo(_:) function, instantiate your new view controller (using its storyboard ID).

5. Set the view controller’s modalPresentationStyle to .FullScreen and its modalTransitionStyle to .Default.

6. Configure the presentation controller (if appropriate).

7. Call presentViewController(_:,animated:,completion:).

8. Create a new custom subclass of UIViewController (say, AuthorInfoViewController).

9. Add an @IBAction func done(_: AnyObject!) action.

10.Add a tap gesture recognizer or a Done button to the root view and connect it to the done(_:) function.

11.The done(_:) function will fetch the presenting view controller and dismiss itself.

When you tap the info button, the screen will be replaced with your new view controller. Tapping the Done button or root view will dismiss it.

You know all of this already; you’ve done all of these steps at one time or another in earlier projects. So, instead of repeating yourself, start with a shortcut and then use this as a foundation to explore some variations.

Modal Presentation Style

Begin by adding a segue from the button to the new view controller. Control+click or right-click the info button and drag to the new view controller, as shown in Figure 12-27.

image

Figure 12-27. Connecting a model presentation segue

If you select the present modally choice, you’ll be performing steps 1 through 7 almost exactly. The key here is the modalPresentationStyle property (step 6). When a view controller’s presentation style is set to .FullScreen, it’s requesting to take over the entire screen, and the presentation controller generally honors that request.

But what are your other choices? You have several, as shown here:

· FullScreen

· OverFullScreen

· CurrentContext

· OverCurrentContext

· Popover

· PageSheet

· FormSheet

· Custom

The .OverFullScreen variation presents the view controller full-screen (exactly the same size as .FullScreen) but does not remove the presenting view controller. This subtle difference permits the new view controller to have a semitransparent or blurred background, allowing the presenting view controller’s content to bleed through. It’s a nice effect and has the sense of a temporary overlay rather than completely replacing the existing view. See the exercise in Chapter 17 for an example.

The two current context versions (.CurrentContext and .OverCurrentContext) place the presented view controller so it overlays or replaces the presenting view controller inside a container view controller, most notably a split view controller. In other words, it replaces or overlays a one view controller inside another view controller, instead of usurping the whole interface.

Popovers, page sheets, and form sheets are presentation modes specifically designed for the iPad. (Until iOS 8 they were available only on the iPad.) The presented view controller appears on top of the presenting view controller in a floating sheet (or “bubble”). The existing view controller never goes away but is often disabled and dimmed.

I’m sure your mouse button finger is getting tired deciding which option to pick. Go ahead and select the popover presentation segue choice (see Figure 12-27).1 This segue will set the presented view controller’s modalPresentationStyle to .Popover before the view is presented. Choose an iPad simulator (or device) and run your app. Tap the info button to present the new view controller, as shown on the left in Figure 12-28.

image

Figure 12-28. The popover presentation style on an iPad and iPhone

Hey, that looks pretty nice! Now switch to an iPhone (or similar compact device) and run your app again. Tap the info button. This time you get a full-screen view controller, as shown on the right in Figure 12-28.

While it doesn’t look too bad, it’s not a good interface. For one thing, you never implemented steps 8 through 11, so the user has no way to dismiss this view. Oops.

You could implement steps 8 through 11. That’s a perfectly valid solution and completely appropriate for many circumstances. Instead, let’s take a closer look at why you got a full-screen view controller instead of a popover and what you might do about that.

Making Presentation Suggestions

So, why did your view controller appear in .FullScreen mode on an iPhone? The modalPresentationStyle property is a suggestion of how the view controller wants to be presented, not a requirement. When the view controller is presented, the presentation controller considers the size of the interface, its current trait collection, and other information and decides the best way to present the view controller. This may, or may not, be what the view controller requested.

Generally, the presentation controller will present the view controller in whatever style it requested, except on horizontally compact devices. When the screen is small, the presentation controller turns popover, form sheet, and page sheet requests into the .FullScreen style, on the (sensible) assumption that there’s not enough space on such a tiny device for those interface styles.

Guess what? The presentation controller has a delegate property (see Figure 12-23). The presentation controller’s delegate is queried about what presentation style to use, and you can influence that decision. Let’s do that now.

First, go back to the storyboard, select the info button’s segue, and assign it an identifier of info. You’ll need that to inject a little code into the presentation process. Now go to the FirstViewController.swift file and add the following function:

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
if segue.identifier == "info" {
let presented = segue.destinationViewController as UIViewController
let presentationController = presented.presentationController
presentationController?.delegate = self
} else {
super.prepareForSegue(segue, sender: sender)
}
}

This function is called by the segue just before the new view controller is presented. It first obtains the view controller that will be presented and obtains its presentation controller. You then make this object the presentation controller’s delegate.

For this to work, this view controller has to adopt the UIAdaptivePresentationControllerDelegate protocol, so add that now (new code in bold).

class FirstViewController: UIViewController,
UIAdaptivePresentationControllerDelegate {

Now add this adaptive presentation controller delegate function:

func adaptivePresentationStyleForPresentationController( image
controller: UIPresentationController) -> UIModalPresentationStyle {
return .None
}

Now select an iPhone simulator (or any other compact device) and run your app again. Tap the info button, and you get a popover, like the one on the right in Figure 12-29.

image

Figure 12-29. Forcing a popover presentation on an iPhone

So, what just happened? On a horizontally compact device, the presentation controller defers to its delegate object to determine how the interface should be adapted to the smaller device. If the delegate returns .None, it means “Don’t adapt the interface. Present it exactly as it was requested.” And that’s what the presentation controller did, putting the new view controller in a popover—an interface you don’t often see on an iPhone.

This particular interface is small enough that it actually works in a popover, even on the smallest mobile devices. But what if it didn’t? Yes, you can still go back and implement steps 8 through 11, but the presentation controller delegate has one more trick up its sleeve.

Adapting a View Controller During Presentation

In addition to getting the delegate’s opinion about the best way to present a view controller in a compact environment, the presentation controller also lets the delegate adapt the interface before presenting it. I’ll talk a lot more about adapting in the next sections, but for now just know that “adapting” an interface means adjusting it so it fits its new home.

Return to the FirstViewController.swift file and adjust the delegate function so it reads like this (modified code in bold):

func adaptivePresentationStyleForPresentationController(
controller: UIPresentationController) -> UIModalPresentationStyle {
return .FullScreen
}

Now your delegate agrees with the presentation controller: when presented on a horizontally compact device, all view controllers (for this delegate) will be presented full-screen. That puts you right back into the pickle you had before—there’s nothing that lets the user dismiss the view. Have no fear, there’s a delegate method for that.

Before the view controller is presented, the presentation controller calls the presentController(_:,viewControllerForAdaptivePresentationStyle:) delegate function. Add one to your class.

func presentationController(controller: UIPresentationController, image
viewControllerForAdaptivePresentationStyle style: image
UIModalPresentationStyle) -> UIViewController? {
let presentedVC = controller.presentedViewController
let replacementController image
= UINavigationController(rootViewController: presentedVC)
let navigationItem = presentedVC.navigationItem
let doneButton = UIBarButtonItem(barButtonSystemItem: .Done,
target: self,
action: "dismissInfo:")
navigationItem.rightBarButtonItem = doneButton
navigationItem.title = "Author"
return replacementController
}

In this function, you can take the view controller to present and modify (adapt) it so it’s ready to be presented full-screen—or whatever the style parameter says it’s going to be presented as. This might entail adding a Done button or adding a tap gesture recognizer and a label that says “tap to dismiss.” It’s entirely up to you. Just make the modifications here and return the modified view controller.

Your function, however, goes one step further; it replaces the view controller with a completely different one! Actually, it creates a navigation view controller and makes the presented view controller its root view. It then configures the navigation bar so it has a Done button, which calls adismissInfo(_:) function to dismiss the view controller. This neatly solves the need to provide a way to dismiss the view, with a standard and recognizable interface, without modifying the original view controller.

Oh, I guess you’ll need that dismissInfo(_:) function.

@IBAction func dismissInfo(sender: AnyObject) {
dismissViewControllerAnimated(true, completion: nil)
}

While still running an iPhone simulator, run your app again, as shown in Figure 12-30.

image

Figure 12-30. Adapted full-screen view controller presentation

Technically, this is adaption, which is the last section of this chapter. But before I segue to that topic (apologies for the pun), let’s take stock about what you’ve learned about presentation controllers and talk about other reasons to use them.

· The view controller that wants to present another view controller is the presenting view controller.

· The view controlling being presented is the presented view controller.

· A presentation controller manages the presentation, adaption, and dismissal of a view controller.

· A view controller has a preferred presentation style, transition style, and size.

· The presentation controller considers these properties when presenting the view controller but may overrule them.

· The presentation controller’s delegate may—in horizontally compact environments—overrule its style decisions and can alter or replace the view controller being presented.

Popover Presentation Controllers

Unless you plan to modify or customize how your view controller is presented, you normally don’t need to mess with the presentation controller. But there’s one notable exception: the popover presentation controller.

Depending on the modalPresentationStyle value, the presentation controller created for your view controller may be a subclass of UIPresentationController with additional features. Specifically, if your view controller’s modalPresentationStyle is .Popover, itspresentationController property will return a UIPopoverPresentationController object. This class has additional functions and properties that apply only to popover presentations—some of which you must set up or the presentation will fail.

You’ve already had to deal with this in earlier projects, so let’s relive one of those moments. When you present a view controller in a popover, you must set its source. This is an object or rectangle that determines where the popover is anchored on the screen. You did this a couple of times in the MyStuff project, back in Chapter 9. Here’s that code again:

if let popover = alert.popoverPresentationController {
popover.sourceView = imageView
popover.sourceRect = imageView.bounds
popover.permittedArrowDirections = ( .Up | .Down )
}

The UIViewController’s convenience property popoverPresentationController is the best way to get the popover controller. This property will be the presentationController if, and only if, it’s a popover controller conveniently downcast toUIPopoverPresentationController. If not, it returns nil. Note that this is the same object returned by presentationController, just downcast.

At a minimum, you must set either the sourceView and sourceRect or the barButtonItem of the popover controller. When you use a segue, as you did earlier, the segue does this for you. You can optionally customize the allowed arrow directions, the color of the arrow (so it matches the background of your popover view’s background), and other features.

Now that you know how to use the presentation controller, let’s get back to adaption.

Adapting View Controller Content

The last essential view controller skill to master is how to adapt your view’s content to different display environments. Adapting a view controller consists of modifying its view objects and layout to present a pleasing user interface for a specific device, screen size, orientation, or resolution. You’ve encountered this repeatedly throughout this book, but now it’s time to dive into the details.

ADAPTION BEFORE IOS 8

Adaption isn’t new to iOS, but iOS 8 introduces a radically different philosophy and a set of tools to help you. In the past, adaption was handled in a somewhat ad hoc fashion. There were properties that told you the broad category of device your app was running on (iPhone or iPad), properties and events that told you what orientation the device was in (portrait, landscape left, landscape right, upside down), properties that told you what size the screen was, and so on.

Your storyboard files were also split, with one storyboard file for iPhone and iPhone-like devices (Main_iPhone.storyboard) and a second storyboard file for iPad and iPad-like devices (Main_iPad.storyboard). Would you like to add a button? You’d have to add the button, connect it to an outlet, connect its action, and create its constraints. And then you’d have to do that all over again in the second storyboard file. Your code would also be riddled with statements such as if deviceIdiom == .iPhone { do this for an iPhone } else { do that for an iPad }. As new types of devices emerged, with new screen sizes and with new resolutions, managing your app’s interface became untenable. iOS 8 changes all of that.

iOS 8 abstracts the interface environment into a set of generic traits and provides a consistent set of methods for adjusting your content. There are no longer separate functions for resizing the view controller and handing device rotation. Now, all view controller size changes are handled the same way. Broadly, you have four different ways of adapting your views, from the broad to the specific:

· Being clever

· Traits

· Sizes

· Layout events

Let’s explore each one and see how and why you’d use it to adapt your interface.

Smarter Than the Average Bear

I listed “being clever” as one technique for adapting your views. If you’re ingenious, you can often design your interface in such a way that it doesn’t need adapting. It will just adjust itself to fit the available display, whatever that is. I think of these as “self-adapting” interfaces, and it’s always the technique I try to apply first. Return to the Main.storyboard file of your Wonderland app and let’s take a crack at adapting the character detail scene. Add the following constraints:

1. Label

a. Top edge to top layout guide (40 pixels)

b. Center horizontally in container

2. Image view

a. Top edge to label (8 pixels)

b. Bottom edge to text view (standard)

c. Leading and trailing edges to superview (40 pixels each)

3. Text view

a. Pin height at 128 pixels

b. Leading and trailing edges to superview (30 pixels each)

c. Bottom edge to bottom layout guide (standard), as shown in Figure 12-31

image

Figure 12-31. Initial constraints for character detail view

If you study these constraints or just run them in the simulator, you’ll see that they work but not all that well (see Figure 12-32).

image

Figure 12-32. Testing character detail constraints

The constraints in a layout define a kind of algebraic equation. The auto-layout logic first finds all the constants in the equation (the size of the superview, the pinned height of a text view, a constraint that says “this label’s top edge must be exactly 40 pixels below the top layout guide,” and so on). It then identifies the properties that are variable (the height of the image view, the top-edge position of the text view, and so on). It then “solves” the equation by providing values for the variables that satisfy all of the constants. The layout in your character detail view “solves” the equation, but it doesn’t look that great.

So far in this book you’ve created only simple, constant, invariable constraints. Most of the time that’s all you need, but sometimes you need something more flexible, and the auto-layout system is both willing and able. Here are some additional tools at your disposal:

· Constraints can express inequalities.

· Constraints can be prioritized.

· View content is compressible.

· View content “hugs” its margins.

First, constraints don’t have to express an unequivocal rule. You can create a constraint that says “The height of this view must be 80 pixels or more” or “The left edge of this button must be more than 6 pixels beyond the right edge of that label.” The first change you’re going to make is to alter the constraint for the bottom edge of the text view. Select the text view. This will reveal2 the constraints attached to it. Select the small constraint at the bottom (between it and the bottom layout guide), as shown in Figure 12-33. In the attributes inspector, change the Relation property from Equal to Greater Than or Equal.

image

Figure 12-33. Creating a constraint inequlity

Now the auto-layout logic has a new variable to play with. No longer is the bottom edge of the text view required to be exactly 8 pixels above the bottom layout guide. The layout engine can now move it around, as long as it is at least 8 pixels above the bottom layout guide.

Prioritizing Constraints

Constraints are also prioritized. By default, the priority of a new constraint is UILayoutPriorityRequired. This value is 1,000, the highest priority. It means the constraint is required; it must be satisfied. But you can create constraints with lower priorities. If you do, the layout logic will “sacrifice” lower-priority constraints in order to satisfy high-priority ones. iOS defines two other handy priorities, UILayoutPriorityDefaultHigh (750) and UILayoutPriorityDefaultLow (250), for important but not required constraints and nice to have but not important constraints, respectively. You can assign any priority from 1 to 1,000—just avoid 50, that’s reserved for another use.

Intrinsic Size

The auto-layout logic also considers a view’s intrinsic size. View objects with content (labels, text views, image views, buttons, and so on) all have an intrinsic size. This is the size the view needs to be to exactly display its content. For an image view, it’s the size of its image. For a label, it’s the drawn size of its text. If you don’t add constraints that dictate the height or width of a view, auto-layout looks to its intrinsic size and starts with that.

Note If you create a custom subclass of UIView and need auto-layout to consider its intrinsic size, you must override the intrinsicContentSize() -> CGSize function and supply that value.

If the view’s intrinsic size won’t satisfy the other constraints, auto-layout will override its intrinsic size to resolve the layout. When it does this, auto-layout encounters two more sets of priorities: compression resistance and hugging. When the layout tries to make a view smaller, the view “resists” compression, and when it tries to make it bigger, the view’s content “hugs” its margins—in other words, it “resists” expansion.

These priorities are properties of the view. Select the image view in the character detail view and use the size inspector to change its vertical compression resistance to 250 (UILayoutPriorityDefaultLow), as shown in Figure 12-34.

image

Figure 12-34. Changing the compression resistance of an image view

Note Compression resistance and hugging each have two priorities, one for vertical and one for horizontal, making for four priorities in all. A UIButton view, by default, has a low horizontal hugging priority because a button doesn’t mind being made wider. But it has a high vertical hugging priority because it strongly resists being made taller.

Run the app again, as shown in Figure 12-35, and compare the results to those in Figure 12-32. With the lower vertical compression resistance, the layout is more inclined to reduce the size of the image view to satisfy all of the layout constraints. With a variable bottom constraint, the text view is free to float up to join the image view.

image

Figure 12-35. Layout with modified constraint and compression resistance

The priorities assigned to the compression resistance and hugging are the same ones used to rank constraints. For example, if a width constraint is narrower than a view’s intrinsic width, the priority of its horizontal compression resistance is compared to the priority of the constraint to determine which width prevails.

Tip Setting the compression resistance and hugging priority to 1,000 (UILayoutPriorityRequired) is equivalent to pinning the view to its intrinsic size.

Now that you understand a little about how constraints, inequality constraints, intrinsic sizes, and priorities work together, you can begin to design complex layout solutions that smoothly adapt to a variety of environments.

But if auto-layout still isn’t sufficient for your needs, you have lots of other options. The next tool to reach for is adaptive constraints. These depend on traits, so let’s talk about traits next.

Using Trait Collections

A trait is one generic aspect of the interface. Is it roomy or cozy? Is it high resolution or low resolution? Every device, window, and view controller has a set of these traits, called its trait collection, that you can examine to make broad decisions about how your interface should appear. Your view controller’s current trait collection can be obtained through its traitCollection property. Table 12-2 lists the properties of a trait collection.

Table 12-2. Traits in a Trait Collection

Trait

Possible Values

Horizontal size class

Regular or Compact

Vertical size class

Regular or Compact

Interface idiom

iPad or iPhone

Display scale

1.0, 2.0, or 3.0

The two most useful traits are the horizontal and vertical size class. A size class indicates how “roomy” the interface is in that direction. An iPad has a Regular size class in both directions, in all orientations. That’s because an iPad’s display is large and there’s rarely any need to make big changes to your interface to make it fit. An iPhone, in contrast, is Compact horizontally and Regular vertically when it’s in portrait orientation. When in landscape, it’s considered Compact in both directions.

So, why doesn’t an iPhone have a Regular horizontal size class when it’s in landscape? Size classes are more about expectations and usability than physical display sizes. As you learned earlier in this chapter, the presentation controller makes decisions about whether to present a view controller as a popover or in full-screen based on the horizontal size class of the device. Keeping the horizontal size class Compact means your photo picker won’t suddenly appear in a popover on your iPhone. It also means that split view controllers will stay collapsed. Think of size classes as “expectation” classes.

As you’ve seen already, the presentation controller uses size classes to make decisions about what presentation style to use. But they’re used in many more places, and one of those is Interface Builder.

Adding Adaptive Constraints (and Objects)

You can define constraints (and view objects) in Interface Builder that appear in your interface only when they occur in conjunction with a specific combination of size classes. You’ve already done this several times in earlier chapters, and you’ll do it a few more before you’re done. Let’s review one of those.

In Chapter 9, you created multiple sets of alignment constraints for the “bang” buttons in the DrumDub projects, as shown in Figure 12-36.

image

Figure 12-36. Adding constraints for the wCompact/hAny size classes

Since you’ve already gone through this process in detail, I won’t rehash it here. But I will make two points.

First, be careful not to create conflicting constraints. If you add a width constraint to the wCompact/hAny set and a different width constraint (for the same view) in the wCompact/hCompact set, when your view is presented on an iPhone 5 in landscape orientation, your view will have a pair of conflicting constraints. Constraints do not override similar constraints in other sets. The constraints in your view are the union of every set that matches the current environment.

Caution A set of conflicting or incomplete constraints will cause your view’s layout process to fail. Views may be pushed off screen, set with a zero size, or not positioned at all. The results are usually a jumbled mess of view objects.

Another feature is the ability to design view objects that appear only in certain trait environments. Just like constraints, objects added to your design when the size class selector is set to wRegular/hRegular will appear only when your view has a size class of wRegular/hRegular.

While this is handy feature, there are a couple of caveats. Adding one button to the wRegular/hAny set and a different button to the wCompact/hAny set creates two button objects. Both button objects exist for the lifetime of your view controller. When the display appears in a horizontally compact environment, the second button is automatically added to your view, and the first button is removed.

Because they are two different objects, you can’t connect them to the same outlet. If you need an outlet for your button, create two outlets and synthesize a property that returns whichever one is installed, something like the following code:

@IBOutlet var button_Regular: UIButton!
@IBOutlet var button_Compact: UIButton!
var button: UIButton {
if button_Regular.superview != nil {
return button_Regular
}
return button_Compact
}

Probably the best feature of adaptive constraints and objects is that they’re managed for you. If the device changes from a regular to a compact environment, the appropriate constraints are automatically added or removed. Doing this yourself in code requires overriding the appropriate functions. That’s also a good thing to know how to do, so let’s look at that now.

Adapting View Programmatically

You can also adapt your views programmatically in response to a number of environmental changes. Broadly, there are the following three strategies, in increasing order of granularity:

· Trait changes

· Size changes

· Layout changes

You’ll need to consider programmatic adaption when you need to adjust the properties of views or make other layout changes that can’t be expressed as constraints. The adaptive constraints in Interface Builder are great, but you can only add or remove constraints and objects. Interface Builder won’t let you define adaptive properties or actions. You can’t, for example, have an image view left-justify its image in a horizontally regular environment and center it in a compact environment. For that, you’ll need a bit of code.

Adapting When Traits Change

The text of your book looks better when the font is little smaller on compact devices, like an iPhone. Modify the code in BookViewController.swift to adjust the font size based on the horizontal size class of the device.

The first step is to create the code to adapt your views. Add this function to your BookViewController class:

func adaptViewsToTraitCollection(traits: UITraitCollection) {
let compactWidth = ( traitCollection.horizontalSizeClass == .Compact )

var fontSize: CGFloat = 18.0
if compactWidth {
fontSize = 14.0
}
let paginator = bookSource.paginator
let currentFont = paginator.font
if currentFont.pointSize != fontSize {
paginator.font = currentFont.fontWithSize(fontSize)
}
}

The function examines the horizontal size class of the trait collection. If it is horizontally compact, it changes the font size the paginator uses to 14.0 points. Otherwise, it’s changed to 18.0 points.

When the traits for your view controller changes, iOS calls your controller’s willTransitionToTraitCollection(_:,withTransitionCoordinator:) function. Override that with this code:

override func willTransitionToTraitCollection(newCollection:UITraitCollection,image
withTransitionCoordinator coordinator:UIViewControllerTransitionCoordinator) {
super.willTransitionToTraitCollection(newCollection, image
withTransitionCoordinator: coordinator)
adaptViewsToTraitCollection(newCollection)
}

This is the function to override to adapt your interface to a new trait environment. All this code does is call your adaptViewToTraitCollection() function to adjust your interface.

Note The adaptViewToTraitCollection() function is called only when the traits for your view controller change. Rotating an iPad, for example, does not trigger this function because an iPad is wRegular/hRegular in both portrait and landscape orientations.

This function is also not called when your view first appears. So that it adapts to its initial environment, you’ll need to call your adaption function once when the view is first loaded. While still in your BookViewController class, add a viewWillAppear(_:) function.

override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
adaptViewsToTraitCollection(traitCollection)
}

When the view is about to appear, the code obtains its current trait collection and uses that to adapt the view. Run the app in both iPad and iPhone simulators and compare the results, as shown in Figure 12-37.

image

Figure 12-37. Adaptable font size

Adapting When Size Changes

If you need to adapt your interface more precisely, the next level of granularity is size changes. When the size of your view controller changes, iOS calls its viewWillTransitionToSize(_:withTransitionCoordinator:) function. This happens whenever the device is rotated (resizing from portrait to landscape). It can also occur in other circumstances. For example, when the in-progress call status banner at the top of the iPhone display disappears, your view controller is subtly resized.

Note The willTransitionToTraitCollection(...) and willTransitionToSize(...) functions are defined by the UIContentContainer protocol. Both the view controller and the presentation controller adopt this protocol. You can intercept these events on either object, but since you typically subclass UIViewController and not UIPresentationController, they’re usually overridden in the view controller.

This is the function to override to perform more precise layout and layout changes that depend on being in portrait or landscape orientation.

Adapting When Layout Occurs

The most fine-grained adaption occurs at the layout level. A view controller has two functions.

viewWillLayoutSubviews()
viewDidLayoutSubviews()

The first is called before your views are laid out, and the second one after. The presentation controller has a comparable set of functions, with slightly different names.

containerViewWillLayoutSubviews()
containerViewDidLayoutSubviews()

Override these functions when you need to adapt your design to any change in its layout.

Caution The will/did layout subviews functions can be called numerous times, so be careful about what code you put there. For example, let’s say you have a label that displays a countdown and it’s being updated once a second. Setting a label’s text changes its intrinsic size, which triggers a layout, which will call viewWillLayoutSubviews() and viewDidLayoutSubviews() every second. You must also be careful not to make any changes in viewDidLayoutSubviews() that would trigger a new layout.

Animating Layout Changes

The last step in adapting your view is to make the actual layout changes that reposition and resize the views in your interface. If you’re using constraints or adaptive constraints, this happens automatically. (That’s one more reason to love constraints.) Your views will be gracefully resized to their new layout as the screen rotates.

But what if you’re adapting your view programmatically? Some changes don’t need to be animated. It would be impractical, for example, to animate the font size change for the book. So, the code you just wrote for that is fine.

On the other hand, if you are positioning views programmatically (without using constraints), you’ll need to reposition them when the view size changes, and you’ll want them to animate along with the rest of the changes. To accomplish that, reach for the object in Figure 12-23 that I’ve ignored so far: the transition coordinator.

A transition coordinator is another helper object, used only during transitions. It’s an ephemeral object that’s created just before the view controller is initially presented or before it transitions to a new trait collection or size. Once the transition is complete, the object goes away.

The transition coordinator has one supremely useful function: animateAlongsideTransition(_:,completion:). Call this function from your willTransitionToTraitCollection(...) or willTransitionToSize(...) function with two blocks. The first block is the code that makes the animatable changes to your view. This will make more sense after you read Chapter 11. But in a nutshell, if you change any animatable properties of your view within this code block—notably, its size or position—that change will be smoothly animated.

You’ll do this in Chapter 16, where you’ll create a “dial” view that is positioned programmatically, without using any constraints. When the interface resizes, it will be your responsibility to reposition them.

Oh, I can’t stand it! Let’s jump ahead to Chapter 16 and peek at that code now (the interesting stuff is in bold).

override func viewWillTransitionToSize(size: CGSize, image
withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator!) {
super.viewWillTransitionToSize(size, image
withTransitionCoordinator: coordinator)
animator?.removeAllBehaviors()
coordinator.animateAlongsideTransition( {
(context) in self.positionDialViews()
},
completion: {
(context) in self.attachDialBehaviors()
})
}

Whenever the view controller is resized, this function relocates the “dial” views to their new position. That part is handled by the cleverly named positionDialViews() function. The thrilling bit is that this function is being called inside a code block passed to theanimateAlongsideTransition(_:,completion:) function. When you do this, the transition coordinator will make sure your animated view changes occur in perfect synchronization with the rest of the view changes.

You don’t have to do this. You’re free to not animate your changes—lame. And you’re free to set up your own animation that runs close to, or even after, the transition animation; the transition coordinator can help you with that too. But most of the time you want your interface to morph seamlessly to its new layout, and animateAlongsideTransition(_:,completion:) makes that easy.

Advanced View Controller Topics

This chapter is already ridiculously long, so if you made it this far, you deserve some kind of reward. But before I release you, let me touch on a couple of advanced topics, just so you’ll be aware of them.

Custom Presentations and Transitions

You can customize your view controller presentation and transitions far beyond what I’ve described in this chapter. Would you like your presented view controller to appear from a puff of smoke? When you dismiss it, would you like it to crumple up like a discarded piece of paper? You can do that, and anything else you can think of, by providing your own presentation controller and/or transition coordinator objects.

There are a lot of details, but the basic technique is to set your view controller’s modalPresentationStyle to .Custom. When you do that, iOS expects your view controller to supply a delegate object in its transitioningDelegate property. This object must conform toUIViewControllerTransitioningDelegate, the methods of which may provide your own custom subclass of UIPresentationController and other animation and transitioning objects that will do the work to presenting, transitioning, and dismissing your view controller.

Refer to the View Controller Programming Guide in Xcode’s Documentation and API Reference window for all of the details. I also highly suggest downloading the sample projects that demonstrate these techniques.

Appearance Proxies

You’ve already seen how you can write code, in functions such as viewWillAppear() and viewWillTransitionToTraitCollection(...), to programmatically adjust the contents of your views. You do this for properties that can’t be set in Interface Builder.

But what if you have the same adjustment to the same kind of view object in lots of different places? Do you have to write code to modify every one in every view controller? Let’s say you’re writing a multiplayer game and a player is on either the Rose team or the Periwinkle team. You’d like to background color of all of your buttons and navigation bars to be light red for Rose players and light blue for Periwinkle players every place those views appear in every view controller.

There’s actually a way to do this with appearance proxies. An appearance proxy is an object that will adjust one or more properties of every view object in its class at once. Here’s how it works.

You ask a specific view object class for its appearance proxy.

let navBarAppearanceProxy = UINavigationBar.appearance()

If you set a display property of the proxy object, it sets that property in every UINavigationBar object in your app, even ones that haven’t been loaded yet.

navBarAppearanceProxy.barTintColor = roseTeamColor

Note You can also request an appearance proxy that applies only to objects in a specific trait environment or are contained in a particular view hierarchy.

A proxy cannot affect every property. The properties that are tagged with UI_APPEARANCE_SELECTOR in the framework headers are the ones it can change. I’ve never found an official list, but most of the visual properties (background, tint, font, and so on) that would apply uniformly to multiple view objects can be customized this way.

You can implement your own appearance properties in custom subclasses of UIView, and your class’s appearance proxy will adjust those too. See the documentation for UIAppearance for the details.

Summary

You’ve traveled far in your quest to master iOS app development. View controllers are the foundation of your user interface. And mastering navigation is a key step in developing your app’s user experience.

In this chapter you used all of the major view controller classes: UIViewController, UITableViewController, UINavigationController, UITabBarController, and UIPageViewController. More importantly, you learned the difference between content and container view controllers and how to assemble and connect them using storyboards. You created view controllers stored in a storyboard file programmatically and used that to create a page view controller data source. You also learned the fundamentals of presenting modal view controllers, the objects involved, and how presentation decisions are made. Finally, you explored all the major techniques for adapting your view to ever-changing display environments.

This is a huge accomplishment. It’s so exciting that you should share it with your friends. The next chapter will show you how to do just that.

___________________

1If you already picked a different segue type, just select the segue and use the attributes inspector to change its Segue attribute to Present as Popover.

2If you’ve turned this feature off, select the view and then click the Edit button for the constraint in the size inspector.