Chapter 6. View Controllers

An iOS app’s interface is dynamic, and with good reason. The entire interface needs to fit into a single display consisting of a single window, which in the case of the iPhone can be almost forbiddingly tiny. The solution is to introduce, at will, completely new interface — a new view, possibly with an elaborate hierarchy of subviews — replacing or covering the previous interface.

For this to work, regions of interface material — often the entire contents of the screen — must come and go in an agile fashion that is understandable to the user. There will typically be a logical, structural, and functional relationship between the view that was present and the view that replaces or covers it, and this relationship will need to be maintained behind the scenes, in your code, as well as being indicated to the user: multiple views may be pure alternatives or siblings of one another, or one view may be a temporary replacement for another, or views may be like successive pages of a book. Animation is often used to emphasize and clarify these relationships as one view is superseded by another. Navigational interface and a vivid, suggestive gestural vocabulary give the user an ability to control what’s seen and an understanding of the possible options: a tab bar whose buttons summon alternate views, a back button or a swipe gesture for returning to a previously visited view, a tap on an interface element to dive deeper into a conceptual world, a Done or Cancel button to escape from a settings screen, and so forth.

In iOS, the management of this dynamic interface is performed through view controllers. A view controller is an instance of UIViewController.

Actually, a view controller is most likely to be an instance of a UIViewController subclass; the UIViewController class is designed to be subclassed, and you are very unlikely to use a plain vanilla UIViewController object without subclassing it. You might write your own UIViewController subclass; you might use a built-in UIViewController subclass such as UINavigationController or UITabBarController; or you might subclass a built-in UIViewController subclass such as UITableViewController (Chapter 8).

A view controller manages a single view (which can, of course, have subviews); its view property points to the view it manages. This is the view controller’s main view, or simply its view. A view controller’s main view has no explicit pointer to the view controller that manages it, but a view controller is a UIResponder and is in the responder chain just above its view, so it is the view’s next responder.

View Controller Responsibilities

A view controller’s most important responsibility is its view. A view controller must have a view; it is useless without one. If that view is to be useful, it must somehow get into the interface, and hence onto the screen; a view controller is usually responsible for seeing to that, too, but typically not the view controller whose view this is; rather, this will be taken care of by some view controller whose view is already in the interface. In many cases, this will happen automatically (I’ll talk more about that in the next section), but you can participate in the process, and for some view controllers you may have to do the work yourself. A view that comes may also eventually go, and the view controller responsible for putting a view into the interface will then be responsible also for removing it.

A view controller will typically provide animation of the interface as a view comes or goes. Built-in view controller subclasses and built-in ways of summoning or removing a view controller and its view come with built-in animations. We are all familiar, for example, with tapping something to make new interface slide in from the side of the screen, and then later tapping a back button to make that interface slide back out again. In cases where you are responsible for getting a view controller’s view onto the screen, you are also responsible for providing the animation. And you can take complete charge of the animation even for built-in view controllers.

View controllers, working together, can save and restore state automatically. This feature helps you ensure that if your app is terminated in the background and subsequently relaunched, it will quickly resume displaying the same interface that was showing when the user last saw it.

The most powerful view controller is the top-level view controller. This might be a fullscreen presented view controller, as I’ll explain later in this chapter; but most of the time it will be your app’s root view controller. This is the view controller managing the root view, the view that sits at the top of the view hierarchy, as the one and only direct subview of the main window, acting as the superview for the rest of the app’s interface.

See Chapter 1 — especially “How an App Launches” — to remind yourself of how the root view controller attains its lofty position at launch time. Either manually in your code or automatically by UIApplicationMain, it is instantiated and assigned to the window’s rootViewController property. The window then takes that view controller’s main view, gives it the correct frame (resizing it if necessary), and makes it its own subview.

Tip

In general, your app’s root view controller, once established at launch time, should remain permanently in place. If you think you need to assign a different view controller to the window’s rootViewController later in the life of your app, think again; you should probably revise your app’s view controller architecture instead. A custom container view controller (discussed later in this chapter) is typically a better solution.

The top-level view controller bears ultimate responsibility for some important decisions about the behavior of your app:

Rotation of the interface

The user can rotate the device, and you might like the interface to rotate in response, to compensate. The runtime consults the top-level view controller about whether to permit such rotation.

Manipulation of the status bar

The status bar is actually a secondary window belonging to the runtime. The runtime consults the top-level view controller as to whether the status bar should be present and, if so, whether its text should be light or dark.

Above and beyond all this, view controllers are typically the heart of any app, by virtue of their role in the model–view–controller architecture: view controllers are controllers (hence the name). Views give the user something to tap, and display data for the user to see; they are view. The data itself is model. But the logic of determining, at any given moment, what views are shown, what data those views display, and what the response to the user’s gestures should be, is controller logic. Typically, that means view controller logic. In any app, view controllers will be the most important controllers — frequently, in fact, the only controllers. View controllers are where you’ll put the bulk of the code that actually makes your app do what your app does.

View Controller Hierarchy

There is always one root view controller, along with its view, the root view. There may also be other view controllers, each of which has its own main view. Such view controllers are subordinate to the root view controller. In iOS, there are two subordination relationships between view controllers:

Parentage (containment)

A view controller can contain another view controller. The containing view controller is the parent of the contained view controller; the contained view controller is a child of the containing view controller. A containment relationship between two view controllers is reflected in their views: the child view controller’s view, if it is in the interface at all, is a subview (at some depth) of the parent view controller’s view.

The parent view controller is responsible for getting a child view controller’s view into the interface, by making it a subview of its own view, and (if necessary) for removing it later. Introduction of a view, removal of a view, and replacement of one view with another often involve a parent view controller managing its children and their views.

A familiar example is the navigation interface: the user taps something and new interface slides in from the side, replacing the current interface. Figure 6-1 shows the TidBITS News app displaying a typical iPhone interface, consisting of a list of story headlines and summaries. This interface is managed by a parent view controller (a UINavigationController) with a child view controller whose view is the list of headlines and summaries. If the user taps an entry in the list, the whole list will slide away to one side and the text of that story will slide in from the other side; the parent view controller has acquired an additional child view controller, and has manipulated the views of its children to bring about this animated change of the interface. The parent view controller itself, meanwhile, stays put — and so does its own view, which functions as a stable superview of the child view controllers’ views.

pios 1901
Figure 6-1. The TidBITS News app
Presentation (modal views)

A view controller can present another view controller. The first view controller is the presenting view controller (not the parent) of the second; the second view controller is the presented view controller (not a child) of the first. The second view controller’s view replaces or covers, completely or partially, the first view controller’s view.

The name of this mechanism, and of the relationship between the view controllers involved, has changed over time. In iOS 4 and before, the presented view controller was called a modal view controller, and its view was a modal view; there is an analogy here to the desktop, where a window is modal if it sits in front of, and denies the user access to, the rest of the interface until it is explicitly dismissed. The terms presented view controller and presented view are more recent and more general, but the historical term “modal” still appears in the documentation and in the API.

A presented view controller’s view does indeed sometimes look rather like a desktop modal view; for example, it might have a button such as Done or Save for dismissing the view, the implication being that this is a place where the user must make a decision and can do nothing else until the decision is made. However, as I’ll explain later, that isn’t the only use of a presented view controller.

There is thus a hierarchy of view controllers. There is exactly one root view controller, and it is the only nonsubordinate view controller — it has neither a parent view controller nor a presenting view controller. Any other view controller, if its view appears in the interface, must be either a child view controller of some parent view controller or a presented view controller of some presenting view controller.

Moreover, there is a clear relationship between the view controller hierarchy and the view hierarchy:

  • For a parent view controller and child view controller, the child’s view, if present in the interface, must be a subview of the parent’s view.

  • For a presenting view controller and presented view controller, the presented view controller’s view completely replaces, or is coherently interposed in front of, the presenting view controller’s view.

In this way, the actual views of the interface form a hierarchy dictated by and parallel to some portion of the view controller hierarchy: every view visible in the interface owes its presence to a view controller’s view, either because it is a view controller’s view, or because it’s a subview of a view controller’s view.

It is crucial that your app’s view controller hierarchy and view hierarchy be structured coherently in the way I have just described at every moment of your app’s lifetime. If your app behaves as a good Cocoa citizen, they will be — as I will now illustrate.

Automatic Child View Placement

The place of a view controller’s view in the view hierarchy will often be automatic. You might never need to put a UIViewController’s view into the view hierarchy manually. You’ll manipulate view controllers; their hierarchy and their built-in functionality will construct and manage the view hierarchy for you.

For example, in Figure 6-1, we see two interface elements:

  • The navigation bar, containing the TidBITS logo.

  • The list of stories, which is actually a UITableView.

I will describe how all of this comes to appear on the screen through the view controller hierarchy and the view hierarchy (Figure 6-2):

  • The app’s root view controller is a UINavigationController; the UINavigationController’s view is the window’s sole immediate subview (the root view). The navigation bar is a subview of that view.

  • The UINavigationController contains a second UIViewController — a parent–child relationship. The child is a custom UIViewController subclass (called MasterViewController); its view is what occupies the rest of the window, as another subview of the UINavigationController’s view. That view is the UITableView. This architecture means that when the user taps a story listing in the UITableView, the whole table will slide out, to be replaced by the view of a different UIViewController, while the navigation bar stays.

pios 1902
Figure 6-2. The TidBITS News app’s initial view controller and view hierarchy

In Figure 6-2, notice the word “automatic” in the two large right-pointing arrows associating a view controller with its view. This is intended to tell you how the view controller’s view became part of the view hierarchy. The UINavigationController’s view became the window’s subview automatically, by virtue of the UINavigationController being the window’s rootViewController. The MasterViewController’s view became the UINavigationController’s view’s subview automatically, by virtue of the MasterViewController being the UINavigationController’s child.

Manual Child View Placement

Sometimes, you’ll write your own parent view controller class. In that case, you will be doing the kind of work that the UINavigationController was doing in that example, so you will need to put a child view controller’s view into the interface manually, as a subview (at some depth) of the parent view controller’s view.

I’ll illustrate with another app of mine (Figure 6-3). The interface displays a flashcard containing information about a Latin word, along with a toolbar (the dark area at the bottom) where the user can tap an icon to choose additional functionality.

pios 1903
Figure 6-3. A Latin flashcard app
pios 1904
Figure 6-4. The Latin flashcard app’s initial view controller and view hierarchy

Again, I will describe how the interface shown in Figure 6-3 comes to appear on the screen through the view controller hierarchy and the view hierarchy (Figure 6-4). The app actually contains over a thousand of these Latin words, and I want the user to be able to navigate between flashcards to see the next or previous word; there is an excellent built-in view controller for this purpose, the UIPageViewController. However, that’s just for the card; the toolbar at the bottom stays there, so the toolbar can’t be inside the UIPageViewController’s view. Therefore:

  • The app’s root view controller is my own UIViewController subclass (called RootViewController); its view contains the toolbar, and is also to contain the UIPageViewController’s view. My RootViewController’s view becomes the window’s subview (the root view) automatically, by virtue of the RootViewController’s being the window’s rootViewController.

  • In order for the UIPageViewController’s view to appear in the interface, since it is not the root view controller, it must be some view controller’s child. There is only one possible parent — my RootViewController. My RootViewController must function as a custom parent view controller, with the UIPageViewController as its child. So I have made that happen, and I have therefore also had to put the UIPageViewController’s view manually into my RootViewController’s view.

  • I hand the UIPageViewController an instance of another custom UIViewController subclass (called CardController) as its child, and the UIPageViewController displays the CardController’s view automatically.

Presentation View Placement

Here’s an example of a presented view controller. My Latin flashcard app has a second mode, where the user is drilled on a subset of the cards in random order; the interface looks very much like the first mode’s interface (Figure 6-5), but it behaves completely differently.

pios 1905
Figure 6-5. The Latin flashcard app, in drill mode
pios 1906
Figure 6-6. The Latin flashcard app’s drill mode view controller and view hierarchy

To implement this, I have another UIViewController subclass (called DrillViewController); it is structured very much like RootViewController. When the user is in drill mode, a DrillViewController is being presented by the RootViewController, meaning that the DrillViewController’s interface takes over the screen automatically: the DrillViewController’s view, with its whole subview hierarchy, including the views of the DrillViewController’s children in the view controller hierarchy, replaces the RootViewController’s view and its whole subview hierarchy (Figure 6-6). The RootViewController is still the window’s rootViewController, and its hierarchy of child view controllers remains in place, but the corresponding view hierarchy is not in the interface; it will be returned to the interface automatically when we leave drill mode (because the presented DrillViewController is dismissed), and the situation will look like Figure 6-4 once again.

Ensuring a Coherent Hierarchy

For any app that you write, for every moment in the lifetime of that app, you should be able to construct a diagram showing the hierarchy of view controllers and charting how each view controller’s view fits into the view hierarchy. The diagram should be similar to mine! The view hierarchy should run in neat parallel with the view controller hierarchy; there should be no crossed wires or orphan views. And every view controller’s view should be placed automatically into the view hierarchy, except in the following two situations:

You can see the view controller hierarchy in schematic form by pausing in the debugger and giving this incantation:

(lldb) expr -l objc -O -- [UIViewController _printHierarchy]

If I give that command when my Latin flashcard app is in the same state shown in Figure 6-3, here’s the output (omitting some of the information):

<JS_Latin_Vocab_iPhone_3.RootViewController ... >
   | <UIPageViewController ... >
   |    | <JS_Latin_Vocab_iPhone_3.CardController ... >

The analysis accords with mine: The window’s root view controller is my RootViewController, which has a child UIPageViewController, which has a child CardController.

Another way to inspect the view controller hierarchy is the view debugger (Figure 6-7). This provides even more information; in addition to the view controller hierarchy, the corresponding view hierarchy is displayed: the window contains the root view controller’s view, the root view controller’s view contains the toolbar and the UIPageViewController’s view, and the page view controller’s view contains the CardController’s view.

pios 1903debug
Figure 6-7. The view debugger displays the view controller hierarchy
Tip

What you’re really doing by following the rules about the view controller and view hierarchies is ensuring a coherent responder chain. Together, these two hierarchies constitute the bulk of the responder chain.

View Controller Creation

A view controller is an instance like any other, and it is created like any other instance — by instantiating its class. You might perform this instantiation in code; in that case, you will of course have to initialize the instance properly as you create it. Here’s an example from one of my own apps:

let llc = LessonListController(terms: self.terms)
let nav = UINavigationController(rootViewController: llc)

In that example, LessonListController is my own UIViewController subclass, so I have called its designated initializer, which I myself have defined; UINavigationController is a built-in UIViewController subclass, and I have used one of its convenience initializers.

Alternatively, a view controller instance might come into existence through the loading of a nib. To make it possible to get a view controller into the nib in the first place, view controllers are included among the object types available through the Library in the nib editor. For example, a scene in a storyboard contains a view controller; in the built app, that view controller will be stored in a nib, and when the app runs, if that view controller is needed, that nib will be loaded to obtain that view controller instance.

Once a view controller comes into existence, it must be retained so that it will persist. This will happen automatically when the view controller is assigned a place in the view controller hierarchy that I described in the previous section. For example:

  • A view controller assigned as a window’s rootViewController is retained by the window.

  • A view controller assigned as another view controller’s child is retained by the parent view controller.

  • A presented view controller is retained by the presenting view controller.

The retaining view controller then takes ownership, and will release the other view controller in good order if and when it is no longer needed.

Here’s an example, from one of my apps, of view controllers being instantiated and then being retained by being placed into the view controller hierarchy:

let llc = LessonListController(terms: self.terms) 1
let nav = UINavigationController(rootViewController: llc) 2
self.present(nav, animated: true) 3

That’s the same code I showed a moment ago, extended by one line. It comes from a view controller class called RootViewController. Here’s how view controller creation and memory management works in those three lines:

1

I instantiate LessonListController.

2

I instantiate UINavigationController, and I assign the LessonListController instance to the UINavigationController instance as its child; the navigation controller retains the LessonListController instance and takes ownership of it.

3

I present the UINavigationController instance on self, a RootViewController instance; the RootViewController instance is the presenting view controller, and it retains and takes ownership of the UINavigationController instance as its presented view controller. The RootViewController instance itself is already the window’s rootViewController, and is retained by the window — and so the view controller hierarchy is safely established.

All of this sounds straightforward, but it is worth dwelling on, because things can go wrong. It is quite possible, if things are mismanaged, for a view controller’s view to get into the interface while the view controller itself is allowed to go out of existence. This must not be permitted. If such a thing happens, at the very least the view will apparently misbehave, failing to perform its intended functionality, because that functionality is embodied in the view controller, which no longer exists. (I’ve made this mistake, so I speak from experience here.) If you instantiate a view controller in code, you should immediately ask yourself who will be retaining this view controller.

How a View Controller Obtains Its View

Initially, when it first comes into existence, a view controller has no view. A view controller is a small, lightweight object; a view is a relatively heavyweight object, involving interface elements that can require a significant amount of memory. Therefore, a view controller postpones obtaining its view until it has to do so, namely, when it is asked for the value of its view property. At that moment, if its view property is nil, the view controller sets about obtaining its view. (We say that a view controller loads its view lazily.) Typically, this happens because the time has come to put the view controller’s view into the interface.

In working with a newly instantiated view controller, be careful not to refer to its view property if you don’t need to, since this can trigger the view controller’s obtaining its view prematurely. (As usual, I speak from experience here.) To learn whether a view controller has a view without causing it to load its view, consult its isViewLoaded property. You can also refer to a view controller’s view safely, without loading it, as its viewIfLoaded (an Optional).

As soon as a view controller has its view, its viewDidLoad method is called. If this view controller is an instance of your own UIViewController subclass, viewDidLoad is your opportunity to modify the contents of this view — to populate it with subviews, to tweak the subviews it already has, and so forth — as well as to perform other initializations of the view controller and its properties. viewDidLoad is generally regarded as a valuable place to put initialization code, because it is one of the earliest events in the life of the view controller instance, and it is called only once in the instance’s lifetime.

When viewDidLoad is called, the view controller’s view property is pointing to the view, so it is safe to refer to self.view. Bear in mind, however, that the view may not yet be part of the interface! In fact, it almost certainly is not. (To confirm this, check whether self.view.window is nil.) Thus, for example, you cannot necessarily rely on the dimensions of the view at this point to be the dimensions that the view will assume when it becomes visible in the interface. Performing dimension-dependent customizations prematurely in viewDidLoad is a common beginner mistake.

Before viewDidLoad is called, the view controller must obtain its view. The question of where and how the view controller will get its view is often crucial. In some cases, to be sure, you won’t care about this; in particular, when a view controller is an instance of a built-in UIViewController subclass such as UINavigationController or UITabBarController, its view is out of your hands — you might never have cause to refer to this view over the entire course of your app’s lifetime — and you simply trust that the view controller will generate its view somehow. But when the view controller is an instance of your own subclass of UIViewController, and when you yourself will design or modify its view, it becomes essential to understand the process whereby a view controller gets its view.

This process is not difficult to understand, but it is rather elaborate, because there are multiple possibilities. Most important, this process is not magic. Yet it quite possibly causes more confusion to beginners than any other matter connected with iOS programming. Therefore, I will explain it in detail. The more you know about the details of how a view controller gets its view, the deeper and clearer will be your understanding of the entire workings of your app, its view controllers, its .storyboard and .xib files, and so on.

The main alternatives are as follows:

  • The view may be instantiated in the view controller’s own code, manually.

  • The view may be created as an empty generic view, automatically.

  • The view may be loaded from a nib file (which could be a storyboard).

In the rest of this section, I’ll demonstrate each of these three ways in which a view controller can obtain its view. For purposes of the demonstration, we’ll need a view controller that we instantiate manually (as opposed to a view controller that comes automatically from a storyboard, as explained in the next section). Since I haven’t yet described how to do anything with a view controller other than make it the window’s rootViewController, this view controller will be the window’s rootViewController.

If you want to follow along with hands-on experimentation, you can start with a clean project created from the Single View App template. The template includes a storyboard and a UIViewController subclass called ViewController, but we’re going to ignore both of those, behaving as if the storyboard didn’t exist: our code will explicitly create an instance of a different UIViewController subclass — which I’ll call RootViewController — and make it the root view controller (as described in Chapter 1 and Appendix B). When you launch the project, you’ll see RootViewController’s view, thus proving that the view controller has successfully obtained its view.

Manual View

To supply a UIViewController’s view manually, in code, override its loadView method. Your job here is to obtain an instance of UIView (or a subclass of UIView) — typically by instantiating it directly — and assign it to self.view. You must not call super (for reasons that I’ll make clear later on).

Let’s try it:

  1. We need a UIViewController subclass, so choose File → New → File; specify iOS → Source → Cocoa Touch Class. Click Next.

  2. Name the class RootViewController, and specify that it is to be a UIViewController subclass. Uncheck “Also create XIB file” (if it happens to be checked). Click Next.

  3. Confirm that we’re saving into the appropriate folder and group, and that these files will be part of the app target. Click Create.

We now have a RootViewController class, and we proceed to edit its code. In RootViewController.swift, we’ll implement loadView. To convince ourselves that the example is working correctly, we’ll give the view that we create manually an identifiable color, and we’ll put some interface inside it, namely a “Hello, World” label:

override func loadView() {
    let v = UIView()
    v.backgroundColor = .green
    self.view = v // *
    let label = UILabel()
    v.addSubview(label)
    label.text = "Hello, World!"
    label.autoresizingMask = [
        .flexibleTopMargin,
        .flexibleLeftMargin,
        .flexibleBottomMargin,
        .flexibleRightMargin]
    label.sizeToFit()
    label.center = CGPoint(v.bounds.midX, v.bounds.midY)
    label.frame = label.frame.integral
}

The starred line is the key: we made a view and we assigned it to self.view. In order to see that that code works, we need to instantiate RootViewController and place that instance into our view controller hierarchy. As I explained a moment ago, we’ll do that by making RootViewController the app’s root view controller. Edit AppDelegate.swift to look like this:

import UIKit
@UIApplicationMain
class AppDelegate : UIResponder, UIApplicationDelegate {
    var window : UIWindow?
    func application(_ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions:
        [UIApplication.LaunchOptionsKey : Any]?) -> Bool {
            self.window = self.window ?? UIWindow()
            let theRVC = RootViewController() // *
            self.window!.rootViewController = theRVC // *
            self.window!.backgroundColor = .white
            self.window!.makeKeyAndVisible()
            return true
        }
}

Again, the starred lines are the key: we instantiate RootViewController and make that instance the app’s root view controller. Build and run the app. Sure enough, there’s our green background and our “Hello, world” label!

When we created our view controller’s view (self.view), we never gave it a reasonable frame. This is because we are relying on someone else to frame the view appropriately. In this case, the “someone else” is the window, which responds to having its rootViewController property set to a view controller by framing the view controller’s view appropriately as the root view before putting it into the window as a subview. In general, it is the responsibility of whoever puts a view controller’s view into the interface to give the view the correct frame — and this will never be the view controller itself (although under some circumstances the view controller can express a preference in this regard). Indeed, the size of a view controller’s view may be changed as it is placed into the interface, and you must keep in mind, as you design your view controller’s view and its subviews, that this can happen. (That’s why, in the preceding code, I used autoresizing to keep the label centered in the view, no matter how the view may be resized.)

Generic Automatic View

We should distinguish between creating a view and populating it. The preceding example fails to draw this distinction. The lines that create our RootViewController’s view are merely these:

let v = UIView()
self.view = v

Everything else configures and populates the view, turning it green and putting a label into it. A more appropriate place to populate a view controller’s view is its viewDidLoad implementation, which, as I’ve already mentioned, is called after the view exists and can be referred to as self.view. We could therefore rewrite the preceding example like this (just for fun, I’ll use autolayout this time):

override func loadView() {
    let v = UIView()
    self.view = v
}
override func viewDidLoad() {
    super.viewDidLoad()
    let v = self.view!
    v.backgroundColor = .green
    let label = UILabel()
    v.addSubview(label)
    label.text = "Hello, World!"
    label.translatesAutoresizingMaskIntoConstraints = false
    NSLayoutConstraint.activate([
        label.centerXAnchor.constraint(equalTo:v.centerXAnchor),
        label.centerYAnchor.constraint(equalTo:v.centerYAnchor)
    ])
}

But if we’re going to do that, we can go even further and remove our implementation of loadView entirely! It turns out that if you don’t implement loadView, and if no view is supplied in any other way, then UIViewController’s default implementation of loadView will do exactly what we are doing: it creates a generic UIView object and assigns it to self.view.

If we needed our view controller’s view to be a particular UIView subclass, that wouldn’t be acceptable; but in this case, our view controller’s view is a generic UIView object, so it is acceptable. Comment out or delete the entire loadView implementation from the preceding code, and build and run the app; our example still works!

View in a Separate Nib

In the preceding examples, we supplied and designed our view controller’s view in code. That works, but of course we’re missing out on the convenience of configuring and populating the view by designing it graphically in Xcode’s nib editor. So now let’s see how a view controller can obtain its view, ready-made, from a nib file.

To make this work, the nib file must be properly configured in accordance with the demands of the nib-loading mechanism. The view controller instance will already have been created. It will load the nib, setting itself as the nib’s owner. The nib itself must be prepared to match this situation. In the nib, the owner object must have the same class as the view controller, and its view outlet must point to the view object in the nib. The result is that when the view controller loads the nib, the view instantiated from the nib is assigned to the view controller’s view property automatically.

Suppose the nib is a .xib file. (Storyboards are discussed in the next section.) In a .xib file, the owner object is the File’s Owner proxy object. Therefore, in a .xib file that is to serve as the source of a view controller’s view, the following two things must be true:

  • The File’s Owner proxy object’s class must correspond to the class of the view controller whose view this will be. This will also cause the File’s Owner to have a view outlet.

  • The File’s Owner proxy object’s view outlet must be connected to the view.

Let’s try it. We can use the example we’ve already developed, with our RootViewController class. Delete the implementation of loadView (if you haven’t already) and viewDidLoad from RootViewController.swift, because we want the view to come from a nib and we’re going to populate it in the nib. Then:

  1. Choose File → New → File and specify iOS → User Interface → View. This will be a .xib file containing a UIView object. Click Next.

  2. Name the file MyNib (meaning MyNib.xib). Confirm the appropriate folder and group, and make sure that the file will be part of the app target. Click Create.

  3. Edit MyNib.xib. Prepare it in the way I described a moment ago:

    1. Select the File’s Owner object; in the Identity inspector, set its class to RootViewController.

    2. Connect the File’s Owner view outlet to the View object.

  4. Design the view. To make it clear that this is not the same view we were creating previously, perhaps you should give the view a red background color (in the Attributes inspector). Drag a UILabel into the middle of the view and give it some text, such as “Hello, World!”

When our RootViewController instance wants its view, we want it to load the MyNib nib. To make it do that, we must associate this nib with our RootViewController instance. Recall these two lines from application(_:didFinishLaunchingWithOptions:) in AppDelegate.swift:

let theRVC = RootViewController()
self.window!.rootViewController = theRVC

We’re going to change the first of those two lines. A UIViewController has a nibName property that tells it what nib, if any, it should load to obtain its view. However, we are not allowed to set the nibName property of theRVC (it is read-only). Instead, as we instantiate the view controller, we use the designated initializer, init(nibName:bundle:), like this:

let theRVC = RootViewController(nibName:"MyNib", bundle:nil)
self.window!.rootViewController = theRVC

(The nil argument to the bundle: parameter specifies the main bundle, which is almost always what you want.)

To prove that this works, build and run. The red background appears! Our view controller’s view is being obtained by loading it from the nib.

The eponymous nib

Now I’m going to describe a shortcut, based on the name of the nib. It turns out that if the nib name passed to init(nibName:bundle:) is nil, a nib will be sought automatically with the same name as the view controller’s class. Moreover, UIViewController’s init() turns out to be a convenience initializer: it actually calls init(nibName:bundle:), passing nil for both arguments.

This means, in effect, that we can return to using init() to initialize the view controller, provided that the nib file’s name matches the name of the view controller class. Let’s try it:

  1. Rename MyNib.xib to RootViewController.xib.

  2. Change the code that instantiates and initializes our RootViewController back to what it was before:

    let theRVC = RootViewController()
    self.window!.rootViewController = theRVC

Build and run. It works!

There’s an additional aspect to this shortcut based on the name of the nib. It seems ridiculous that we should end up with a nib that has “Controller” in its name merely because our view controller, as is so often the case, has “Controller” in its name. A nib, after all, is not a controller. It turns out that the runtime, in looking for a view controller’s corresponding nib, will in fact try stripping “Controller” off the end of the view controller class’s name. Thus, we can name our nib file RootView.xib instead of RootViewController.xib, and it will still be properly associated with our RootViewController instance.

When we created our UIViewController subclass, RootViewController, we saw in the Xcode dialog a checkbox offering to create an eponymous .xib file at the same time: “Also create XIB file.” We deliberately unchecked it. Suppose we had checked it; what would have happened? In that case, Xcode would have created RootViewController.swift and RootViewController.xib. Moreover, it would have configured RootViewController.xib for us: the File’s Owner’s class would already be set to the view controller’s class, RootViewController, and its view outlet would already be hooked up to the view. This view controller and .xib file are thus ready for use together: you instantiate the view controller with a nil nib name, and it gets its view from the eponymous nib.

(The .xib file created by Xcode in response to checking “Also create XIB file” does not have “Controller” stripped off the end of its name. But you can rename it manually later if the default name bothers you.)

Different nibs for different device types

Another convention involving the nib name has to do with the rules for loading resources by name generally. The same naming rule that I mentioned in Chapter 2 for an image file extended by the suffix ~ipad applies to nib files. A nib file named RootViewController~ipad.xib will be loaded on an iPad when a nib named "RootViewController" is sought. This principle can simplify your life when you’re writing a universal app, as you can easily use one nib on iPhone and another nib on iPad — though you might not need to do that, since conditional interface design, described in Chapter 1, may permit you to construct an interface differing on iPad and iPhone in a single nib.

Summary

We are now in a position to summarize the sequence whereby a view controller’s view is obtained. It turns out that the entire process is driven by loadView:

  1. When the view controller first decides that it needs its view, loadView is always called:

    • If we override loadView, we supply and set the view in code, and we do not call super. Therefore, the process of seeking a view comes to an end.

    • If we don’t override loadView, UIViewController’s built-in default implementation of loadView takes over, and performs the rest of the process.

  2. UIViewController’s default implementation of loadView looks for a nib:

    • If the view controller was instantiated with an explicit nibName:, a nib with that name is sought, and the process comes to an end. (If there is no such nib, the app will crash at launch.)

    • If the view controller was instantiated with a nil nibName:, then:

      1. An eponymous nib is sought. If it is found, it is loaded and the process comes to an end.

      2. If the view controller’s name ends in “Controller,” an eponymous nib without the “Controller” is sought. If it is found, it is loaded and the process comes to an end.

  3. If we reach this point, UIViewController’s default implementation of loadView creates a generic UIView.

How Storyboards Work

A storyboard typically uses the third approach to supply a view controller with its view: the view is loaded from a nib. It’s just like what we did a moment ago with RootViewController and its .xib file — but the storyboard conceals the nib so that you don’t see it as a separate file. In fact, the nib isn’t even created until you build the app. At that moment, the storyboard is compiled into a .storyboardc bundle, and each scene in the storyboard, representing a view controller and its view, is split into two nibs:

View controller nib

The first nib contains just the view controller.

View nib

The second nib contains the view, its subviews, and any other top-level objects such as gesture recognizers. The view nib has a special name, such as 01J-lp-oVM-view-Ze5-6b-2t3.nib. It is correctly configured: its File’s Owner class is the view controller’s class, with its view outlet hooked to the view.

As a result of this architecture, a storyboard has all the memory management advantages of nib files: none of these nib files are loaded until the instances that they contain are needed, and they can be loaded multiple times to give additional instances of the same nib objects. At the same time, you get the convenience of being able to see and edit a lot of your app’s interface simultaneously in one place.

How a View Controller Nib is Loaded

A storyboard is, first and foremost, a source of view controller instances. In fact, you can set up your app in such a way that a storyboard is the source of every view controller that your app will ever instantiate; indeed, by appropriate configuration of your storyboard, you can usually arrange that every view controller your app will ever need will be instantiated automatically at exactly the moment it is needed.

Every storyboard scene, as I’ve just said, generates at build time a nib containing a view controller. To instantiate a view controller from that nib, we have only to load the nib. The view controller is the nib’s sole top-level object. Loading a nib provides a reference to the instances that come from the nib’s top-level objects, so now we have a reference to the view controller instance.

Loading a view controller nib from a storyboard starts with a reference to the storyboard. You can get a reference to a storyboard either by calling the UIStoryboard initializer init(name:bundle:) or through the storyboard property of a view controller that has already been instantiated from that storyboard.

When a view controller needs to be instantiated from a storyboard, its nib can be loaded in one of four main ways:

Initial view controller

At most one view controller in the storyboard is designated the storyboard’s initial view controller (also called its entry point). To instantiate that view controller, call the UIStoryboard instance method instantiateInitialViewController. The view controller instance is returned. (For an app with a main storyboard, that’s what UIApplicationMain does automatically at launch time.)

By identifier

A view controller in a storyboard can be assigned an arbitrary string identifier; this is its Storyboard ID in the Identity inspector. To instantiate that view controller, call the UIStoryboard instance method instantiateViewController(withIdentifier:). The view controller instance is returned.

By relationship

A parent view controller in a storyboard may have immediate children, such as a UINavigationController and its initial child view controller. The nib editor will show a relationship connection between them. When the parent is instantiated (the source of the relationship), the initial children (the destination of the relationship) are automatically instantiated at the same time.

By a triggered segue

A view controller in a storyboard may be the source of a segue whose destination is a child view controller or a presented view controller. When the segue is triggered and performed, it automatically instantiates the destination view controller.

I’ll go into greater detail about storyboards and segues later in this chapter.

How a View Nib is Loaded

Let’s say now that, one way or another, a view controller has just been instantiated from its storyboard nib. It has no view! Views are loaded lazily, as we know. Sooner or later, the view controller will probably want its view (typically because it is time to put that view into the interface). How will it get it?

The view nib, as I already mentioned, has been assigned a special name, such as 01J-lp-oVM-view-Ze5-6b-2t3.nib. It turns out that the view controller, in its nib, was handed that same special name: its nibName property was set to the name of the view nib. Thus, when the view controller wants its view, it loads it in the normal way! It has a non-nil nibName, so it looks for a nib by that name — and finds it. The nib is loaded and the view becomes the view controller’s view.

The default scene structure is that a view controller in a storyboard scene contains its view — but you don’t have to use that structure. You can select the view inside a view controller in a storyboard and delete it! If you do that, then that view controller won’t have a corresponding view nib; instead the view controller will have to obtain its view in one of the other ways we’ve already discussed: by an implementation of loadView in the code of that view controller class, or by loading an eponymous nib file (which you supply as a .xib file) — or even, if all of that fails, by creating a generic UIView.

View Resizing

There are several major kinds of occasion when a view controller’s view is likely to be resized:

  • It is put into the interface.

  • The app rotates.

  • The surrounding interface changes; for example, a navigation bar gets taller or shorter, appears or disappears.

As I explained in Chapter 1, if you’re using the nib editor to design your view controller’s main view interface, you’ll almost certainly use layout — most probably autolayout, possibly along with conditional interface (see “Conditional Interface Design”) — to help your app cope with all this resizing, regardless of the view’s size and orientation. If your code also needs to take a hand in responding to a change in the view controller’s view size, that code will likely go into the view controller itself. A view controller has properties and receives events connected to the resizing of its view, so that it can respond when such resizing takes place, and can even help dictate the arrangement of the interface if needed. I’ll talk later about where you’re likely to slot your layout-related code into your view controller.

View Size in the Nib Editor

When you design your interface in the nib editor, every view controller’s view has to be displayed at some definite size. You can specify that you want the view displayed at the size of any actual device. You can also specify an orientation. Using the Simulated Metrics pop-up menus in the Attributes inspector, you can adjust for the presence or absence of interface elements that can affect layout (status bar, top bar, bottom bar). Using the Simulated Size pop-up menu in the Size inspector, you can display the view controller’s view at any size.

But this is only a way of displaying the view. It tells you nothing about the size that the view will be when the app runs. There is a huge range of possible sizes the view may assume when the app runs on different devices, in different orientations, and with different surroundings. Obviously no single device size, orientation, or metrics can reflect all of these. If you design the interface only for the size you see in the nib editor, you can get a rude surprise when you actually run the app and the view appears at some other size! Failing to take account of this possibility is a common beginner mistake.

Be sure to design your app’s interface to be coherent at any size it may actually assume. You can get a pretty good idea of whether you’re doing that successfully, without running on every device type, thanks to the nib editor’s ability to switch between displaying different device sizes (using the View As button at the lower left of the canvas), as well as the Preview display in the assistant pane (“Previewing Your Interface”). If your code also takes a hand in layout, you’ll need to run in the Simulator with at least a few different device sizes to see your code at work.

Bars and Underlapping

A view controller’s view will often have to adapt to the presence of bars at the top and bottom of the screen:

The status bar is underlapped

The status bar is transparent, so that the region of a view behind it is visible through it. The root view, and any other fullscreen view, must occupy the entire window, including the status bar area, the top of the view being visible behind the transparent status bar. You’ll want to design your view so that its top doesn’t contain any interface objects that will be overlapped by the status bar.

Top and bottom bars may be underlapped

The top and bottom bars displayed by a navigation controller (navigation bar, toolbar) or tab bar controller (tab bar) can be translucent. When they are, your view controller’s view is, by default, extended behind the translucent bar, underlapping it. Again, you’ll want to design your view so that this underlapping doesn’t conceal any of your view’s important interface.

The status bar may be present or absent. Top and bottom bars may be present or absent, and, if present, their height can change. How will your interface cope with such changes? The primary coping mechanism is the view controller’s safe area (see Chapter 1). The top and bottom of the safe area move automatically at runtime to reflect the view’s environment:

Safe area top

The safe area’s top is positioned as follows:

  • If there is a status bar and no top bar, at the bottom of the status bar.

  • If there is a top bar, at the bottom of the top bar.

  • If there is no top bar and no status bar, at the top of the view.

Safe area bottom

The safe area’s bottom is positioned as follows:

  • If there is a bottom bar, at the top of the bottom bar.

  • If there is no bottom bar, at the bottom of the view.

The easiest way to involve the safe area in your view layout is through autolayout and constraints. A view vends the safe area as its safeAreaLayoutGuide. By constraining a view to the topAnchor or bottomAnchor of the safeAreaLayoutGuide, you guarantee that the view will move when the safe area changes. Typically, the view whose safeAreaLayoutGuide you’ll be interested in is the view controller’s main view. Such constraints are easy to form in the nib editor — they are the default. In the Size inspector, the main view’s Safe Area Layout Guide is checked; therefore, when you form a constraint from a subview to the main view, the nib editor will offer to configure it with respect to the main view’s safe area. (If you need to form a constraint to the safe area in any other view, check Safe Area Layout Guide in that view’s Size inspector.)

If you need actual numbers in order to perform layout-related calculations, the distances between the safe area boundaries and the view’s edges are reported as the view’s safeAreaInsets. If you need to impose larger safe area insets, set the view controller’s additionalSafeAreaInsets.

Status bar

The default behavior of the status bar is that it is present, except in landscape orientation on an iPhone, where it is absent. The top-level view controller — which is usually the root view controller — gets a say in this behavior; it also determines the look of the status bar when present. Your UIViewController subclass, if an instance of it is the top-level view controller, can exercise this power by overriding these properties:

preferredStatusBarStyle

Your choices (UIStatusBarStyle) are .default and .lightContent, meaning dark text and light text, respectively. Use light text for legibility if the view content underlapping the status bar is dark.

prefersStatusBarHidden

A value of true makes the status bar invisible; a value of false makes the status bar visible, even in landscape orientation on an iPhone.

Your override will be a computed variable with a getter function; your getter can return the result of a call to super to get the default behavior.

Even if your view controller is not the top-level view controller, those properties might still be consulted and obeyed. Your view controller might be the child of a parent that is the top-level view controller and delegates the decision-making power to its child, through an override of these properties:

childForStatusBarStyle
childForStatusBarHidden

Used to delegate the decision on the status bar style or visibility to a child view controller’s preferredStatusBarStyle or prefersStatusBarHidden.

For example, a tab bar controller implements those properties to allow your view controller to decide the status bar style and visibility when your view controller’s view occupies the tab bar controller’s view. Thus, your view controller gets to make the decisions even though the tab bar controller is the top-level view controller.

You are not in charge of when status bar–related properties are consulted, but you can provide a nudge: if the situation has changed and one of these properties would now give a different answer, call setNeedsStatusBarAppearanceUpdate on your view controller. If this call is inside an animations function, the change in the look of the status bar will be animated with the specified duration. The character of the animation from visible to invisible (and vice versa) is set by your view controller’s override of preferredStatusBarUpdateAnimation; the value you return (UIStatusBarAnimation) can be .fade, .slide, or .none.

When you toggle the visibility of the status bar, the top of the safe area may move up or down. If your main view has subviews with constraints to the safe area’s top anchor, those subviews will move. If this happens when the main view is visible, the user will see this movement as a jump. That is probably not what you want. To prevent it, animate the change in layout (“Animation and Layout”): call layoutIfNeeded on your view in the same animations function in which you call setNeedsStatusBarAppearanceUpdate. Your layout changes will be animated together with the change in status bar visibility. In this example, a button’s action method toggles the visibility of the status bar with smooth animation:

var hide = false
override var prefersStatusBarHidden : Bool {
    return self.hide
}
@IBAction func doButton(_ sender: Any) {
    self.hide.toggle()
    UIView.animate(withDuration:0.4) {
        self.setNeedsStatusBarAppearanceUpdate()
        self.view.layoutIfNeeded()
    }
}

Extended layout

If your UIViewController’s parent is a navigation controller or tab bar controller, you can govern whether its view underlaps a top bar (navigation bar) or bottom bar (toolbar, tab bar) with these UIViewController properties:

edgesForExtendedLayout

A UIRectEdge. The default is .all, meaning that this view controller’s view will underlap a translucent top bar or a translucent bottom bar. The other extreme is .none, meaning that this view controller’s view won’t underlap top and bottom bars. Other possibilities are .top (underlap translucent top bars only) and .bottom (underlap translucent bottom bars only).

extendedLayoutIncludesOpaqueBars

If true, then if edgesForExtendedLayout permits underlapping of bars, those bars will be underlapped even if they are opaque. The default is false, meaning that only translucent bars are underlapped.

Resizing and Layout Events

A UIViewController receives events that notify it of changes related to the main view being resized:

viewWillTransition(to:with:) (UIContentContainer protocol)

Sent when the main view’s size is about to change. In particular, the app might be about to undergo rotation, or the user might widen or narrow the app under iPad multitasking (see Chapter 9). The first parameter is the new size (a CGSize). The old size is still available as self.view.bounds.size. This event is not sent on launch or when your view controller’s view is first embedded into the interface. If you override this method, call super.

willTransition(to:with:) (UIContentContainer protocol)

Sent when the app’s trait collection is about to change. This could be due to a size change that causes the size classes to change — for example, the app rotates 90 degrees on an iPhone. (However, the trait collection can change for reasons having nothing to do with rotation or size classes; for example, the user changing the dynamic text size setting constitutes a trait collection change, as I’ll explain in Chapter 10.) The first parameter is the new trait collection (a UITraitCollection). The old trait collection is still available as self.traitCollection. This event is not sent on launch or when your view controller’s view is first embedded into the interface. If you override this method, call super.

traitCollectionDidChange(_:) (UITraitEnvironment protocol)

Sent after the trait collection changes, including on launch or when the trait collection is set for the first time. The parameter is the old trait collection, and may be nil when this event first arrives; the new trait collection is available as self.traitCollection.

The with: parameter in the first two methods is a transition coordinator (UIViewControllerTransitionCoordinator). If we’re getting these events because rotation is about to take place, we can hook into the rotation animation by calling this method of the coordinator:

animate(alongsideTransition:completion:)

The first parameter is an animations function; animations we supply here will be performed in coordination with the rotation animation. The second parameter is an optional completion function to be executed when the rotation animation is over.

In addition, a UIViewController receives these events related to the layout of its view (and compare “Layout Events” for the corresponding UIView events):

updateViewConstraints

The view is about to be told to update its constraints (updateConstraints), including at application launch. If you override this method, call super.

viewWillLayoutSubviews
viewDidLayoutSubviews

These events surround the moment when the view is sent layoutSubviews, including at application launch.

Layout events are actually a more reliable way to detect a size change than the resizing events. There are many circumstances, such as the showing and hiding of a navigation bar that isn’t underlapped, under which your view can be resized without a size change being reported. (I regard this as a flaw in the iOS view controller event architecture.) But under those same circumstances you’ll always get layout events, such as viewWillLayoutSubviews.

In a situation where all these events are sent, the order is:

  1. willTransition(to:with:) (the trait collection)

  2. viewWillTransition(to:with:) (the size)

  3. updateViewConstraints

  4. traitCollectionDidChange(_:)

  5. viewWillLayoutSubviews

  6. viewDidLayoutSubviews

There is no guarantee that any of these events, if sent, will be sent exactly once. Your code should take some care to do nothing if nothing relevant has changed.

Rotation

Your app can rotate, moving its top to correspond to a different edge of the device’s screen. Your view controller will be intimately concerned with rotation of the app.

Tip

You can toggle whether a Simulator window should automatically rotate if the orientation of the app changes. Choose Hardware → Rotate Device Automatically to toggle this setting. Your choice applies to all Simulator windows.

Detecting Rotation

Rotation expresses itself in two ways:

The status bar orientation changes

You can hear about this by way of these app delegate events and notifications:

  • application(_:willChangeStatusBarOrientation:duration:)

  • UIApplication.willChangeStatusBarOrientationNotification

  • application(_:didChangeStatusBarOrientation:)

  • UIApplication.didChangeStatusBarOrientationNotification

The current orientation (which is also the app’s current orientation) is available from the UIApplication as its statusBarOrientation; the app delegate methods provide the other orientation (the one we are changing to or from, respectively) as the second parameter, and the notifications provide it in the userInfo under the UIApplication.statusBarOrientationUserInfoKey. Possible values (UIInterfaceOrientation) are:

  • .portrait

  • .portraitUpsideDown

  • .landscapeLeft

  • .landscapeRight

Two convenient UIInterfaceOrientation properties, .isLandscape and .isPortrait, return a Bool.

The view controller’s view is resized

The view controller receives events related to resizing and layout, described in the preceding section.

On the whole, you will probably never concern yourself with the status bar orientation; the resizing events are preferable, even though they are not ideal signals that rotation is taking place. Resizing can be caused in other ways, and you will want to respond to the resizing, not to the mere fact that the status bar orientation is changing. Rotation will always cause a size change event; if it causes the size classes to change, it will cause a trait collection change event; and it will always trigger layout of the view controller’s main view.

Uses of Rotation

There are two complementary uses of rotation:

Compensatory rotation

The app rotates to compensate for the orientation of the device, so that the app appears right way up with respect to how the user is holding the device.

Forced rotation

The app rotates when a particular view appears in the interface, or when the app launches, to indicate that the user needs to rotate the device in order to view the app the right way up. This is typically because the interface has been specifically designed, in view of the fact that the screen is not square, to appear in just one orientation (portrait or landscape).

In the case of the iPhone, no law says that your app has to perform compensatory rotation. Most of my iPhone apps do not do so; indeed, I have no compunction about doing just the opposite, namely forced rotation. My view controller views often look best in just one orientation (either just portrait or just landscape), and they stubbornly stay there regardless of how the user holds the device; therefore, my app forces the user to rotate the device differently depending on what view is being displayed. This is reasonable, because the iPhone is small and easily reoriented with a twist of the user’s wrist, and it has a natural right way up. Even iPhone apps that do perform compensatory rotation do not generally rotate to an upside-down orientation, because the user is unlikely to hold the device that way.

On the other hand, Apple thinks of an iPad as having no natural top, and would prefer iPad apps to rotate to at least two opposed orientations (such as both landscape orientations), and preferably to all four possible orientations, so that the user isn’t restricted in how the device is held.

It’s trivial to let your app rotate to two opposed orientations, because once the app is set up to work in one of them, it can work with no change in the other. But allowing a single interface to rotate between two orientations that are 90 degrees apart is trickier, because its dimensions must change — roughly speaking, its height and width are transposed — and this may require a change of layout and might even call for more substantial alterations, such as removal or addition of part of the interface. A good example is the behavior of Apple’s Mail app on the iPad: in landscape, the master pane and the detail pane appear side by side, but in portrait, the master pane is removed and must be summoned as a temporary overlay on top of the detail pane (explained in Chapter 9).

Permitting Compensatory Rotation

By default, when you create an Xcode project, the resulting app will perform compensatory rotation in response to the user’s rotation of the device. For an iPhone app, this means that the app can appear with its top at the top of the device or either of the two sides of the device. For an iPad app, this means that the app can assume any of the four orientations.

If the default behavior isn’t what you want, it is up to you to change it. There are three levels at which you can make changes:

  • The app itself, in its Info.plist, may declare once and for all every orientation the interface will ever be permitted to assume. It does this under the “Supported interface orientations” key, UISupportedInterfaceOrientations (supplemented, for a universal app, by “Supported interface orientations (iPad),” UISupportedInterfaceOrientations~ipad). These keys can be set through checkboxes when you edit the app target, in the General tab.

  • The app delegate may implement the application(_:supportedInterfaceOrientationsFor:) method, returning a bitmask listing every orientation the interface is permitted to assume at that moment. This list overrides the Info.plist settings. Thus, the app delegate can do dynamically what the Info.plist can do only statically. application(_:supportedInterfaceOrientationsFor:) is called at least once every time the device rotates.

  • The top-level view controller — that is, the root view controller, or a view controller presented fullscreen — may override the supportedInterfaceOrientations property, returning a bitmask listing a set of orientations that intersects the set of orientations permitted by the app or the app delegate. The resulting intersection will then be the set of orientations permitted at that moment. This intersection must not be empty; if it is, your app will crash (with a useful message: “Supported orientations has no common orientation with the application”). supportedInterfaceOrientations is consulted at least once every time the device rotates.

    The top-level view controller can also override shouldAutorotate. This is a Bool, and the default is true. shouldAutorotate is consulted at least once every time the device rotates; if it returns false, the interface will not rotate to compensate at this moment, and supportedInterfaceOrientations is not consulted.

Warning

Built-in parent view controllers, when they are the top-level view controller, do not automatically consult their children about rotation. If your view controller is a child view controller of a UITabBarController or a UINavigationController, it has no direct say in how the app rotates. Those parent view controllers, however, do consult their delegates about rotation, as I’ll explain later.

You can call the UIViewController class method attemptRotationToDeviceOrientation to prompt the runtime to do immediately what it would do if the user were to rotate the device, namely to walk the three levels I’ve just described and, if the results permit rotation of the interface to match the current device orientation, to rotate the interface. This would be useful if, say, your view controller had previously returned false from shouldAutorotate, but is now for some reason prepared to return true and wants to be asked again, immediately.

The bitmask you return from application(_:supportedInterfaceOrientationsFor:) or supportedInterfaceOrientations is a UIInterfaceOrientationMask. It may be one of these values, or multiple values combined:

  • .portrait

  • .landscapeLeft

  • .landscapeRight

  • .portraitUpsideDown

  • .landscape (a combination of .left and .right)

  • .all (a combination of .portrait, .upsideDown, .left, and .right)

  • .allButUpsideDown (a combination of .portrait, .left, and .right)

For example:

override var supportedInterfaceOrientations : UIInterfaceOrientationMask {
    return .portrait
}

An iPhone would prefer not to permit an app to rotate to .portraitUpsideDown. Therefore, if you include .portraitUpsideDown under “Supported interface orientations” in the Info.plist, it will be ignored. You can work around this, if you really want to, by also including .portraitUpsideDown in the top-level view controller’s supportedInterfaceOrientations.

Note

On iPad, if your app permits all four orientations, and if it doesn’t opt out of iPad multitasking (by setting UIRequiresFullScreen in its Info.plist), then supportedInterfaceOrientations and shouldAutorotate are never consulted, presumably because the answer is known in advance.

If your code needs to know the current physical orientation of the device (as opposed to the current orientation of the app), it is UIDevice.current.orientation. Possible values (UIDeviceOrientation) are .unknown, .portrait, and so on. Two convenient UIDeviceOrientation properties, .isPortrait and .isLandscape, return a Bool.

Initial Orientation

I’ve talked about how to determine what orientations your app can support in the course of its lifetime; but what about its initial orientation, the very first orientation your app will assume when it launches?

In general, an app will launch directly into whatever permitted orientation is closest to the device’s current orientation at launch time. There can be a complication, however, when the orientation in which the device is held would have been legal, but the initial root view controller rules it out.

For example, suppose the device is held in portrait, and the Info.plist permits all orientations, but the root view controller’s supportedInterfaceOrientations is set to return .landscape. Then the app starts to launch into portrait, realizes its mistake, and finishes launching in landscape. On the iPhone, this entails a kind of semirotation: willTransition(to:with:) is sent to report a trait collection change, but there is no size change. I regard this behavior as incoherent.

To work around that problem, here’s a trick that I use. Let’s say that my app needs eventually to be able to rotate to portrait, so I need to permit all orientations, but its initial root view controller must appear only in landscape. In my Info.plist, I permit only landscape; that way, my app launches directly into landscape, no matter how the device is oriented. But in my app delegate’s application(_:supportedInterfaceOrientationsFor:), I return .all; that way, my app can rotate subsequently to portrait if it needs to.

View Controller Manual Layout

A view controller governs its main view, and may well take charge of populating it with subviews and configuring those subviews. What if that involves participating manually in the layout of those subviews? As we have seen, a view controller’s view can be resized, both as the view is first put into the interface and later as the app runs and is rotated. Where should your view controller’s layout code be placed in order to behave coherently in the face of these potential size changes?

Initial Manual Layout

Let’s start with the problem of initial layout. There is a natural temptation to perform initial layout-related tasks in viewDidLoad. This method is extraordinarily convenient. It is guaranteed to be called exactly once in the life of the view controller; that moment is as early as possible in the life of the view controller; and at that time, the view controller has its view, and if it got that view from a nib, properties connected to outlets from that nib have been set. So this seems like the perfect place for initializations. And so it is — but initialization and layout are not the same thing.

Remember, at the time viewDidLoad is called, the view controller’s view has been loaded, but it has not yet been inserted into the interface! The view has not been fully resized for the first time, and initial layout has not yet taken place. Thus, you cannot do anything here that depends upon knowing the dimensions of the view controller’s view or any other nib-loaded view — for the simple reason that you do not know them. Performing layout-related tasks in viewDidLoad and then wondering why things are sized or positioned incorrectly is a common beginner mistake.

Here’s an elementary example. Imagine that our view controller is the child of a navigation controller, and that our view controller does not underlap top and bottom bars. And suppose we wish, in code, before our view controller’s view appears, to create a label and place it in the lower left corner of our main view. This code looks as if it should do that:

override func viewDidLoad() {
    super.viewDidLoad()
    let lab = UILabel()
    lab.text = "Hello"
    lab.sizeToFit()
    lab.frame.origin.y = self.view.bounds.height - lab.frame.height
    self.view.addSubview(lab)
}

But it doesn’t work. The app launches, our main view appears — and there’s no label. Where did the label go? The view debugger reveals the answer: it’s below the bottom of the screen. What happened is that our main view (self.view) was resized; its height was decreased. But that took place after viewDidLoad. So our calculation of the label’s frame.origin.y, which depends on our main view’s bounds.height, ends up being incorrect.

However, you can certainly create and insert this label in viewDidLoad and configure it for future layout. It is perfectly fine, for example, to insert a view and give it autolayout constraints in viewDidLoad:

override func viewDidLoad() {
    let lab = UILabel()
    lab.text = "Hello"
    self.view.addSubview(lab)
    lab.translatesAutoresizingMaskIntoConstraints = false
    NSLayoutConstraint.activate([
        lab.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
        lab.bottomAnchor.constraint(equalTo: self.view.bottomAnchor)
    ])
}

That is not a case of doing manual layout in viewDidLoad. The constraints are not layout; they are instructions as to how this view should be sized and positioned by the runtime when layout does happen. (Autoresizing would work fine here too.)

But let’s say that, for some reason, you want to govern this view’s layout actively and entirely in view controller code. Then the natural place to put that code is an event related to resizing and layout of the main view — such as viewWillLayoutSubviews.

However, we need to be careful, because viewWillLayoutSubviews may be called many times over the life of our view controller. We can position our label in viewWillLayoutSubviews each time it is called, and indeed that’s a good thing to do, because it takes care of repositioning the label when the main view’s size changes on rotation; but we don’t want to create our label more than once. A possible solution is to use an instance property as a flag to make sure we create the label only once; here, I’ll use an Optional, since that also gives me a reference to the label:

var lab : UILabel!
override func viewWillLayoutSubviews() {
    if self.lab == nil {
        self.lab = UILabel()
        self.lab.text = "Hello"
        self.lab.sizeToFit()
        self.view.addSubview(lab)
    }
    self.lab.frame.origin.y = self.view.bounds.height - self.lab.frame.height
}

That works, but it still feels wrong. Surely we should not be creating a view in viewWillLayoutSubviews. Let’s distribute responsibilities properly; we’ll create the view in viewDidLoad and position it in viewWillLayoutSubviews:

var lab : UILabel!
override func viewDidLoad() {
    super.viewDidLoad()
    self.lab = UILabel()
    self.lab.text = "Hello"
    self.lab.sizeToFit()
    self.view.addSubview(lab)
}
override func viewWillLayoutSubviews() {
    self.lab.frame.origin.y = self.view.bounds.height - self.lab.frame.height
}

Manual Layout During Rotation

The same principles apply to manual layout later in the life of the app. A particularly vital use case is that rotation is taking place, and we want to respond by changing the layout in some way. Once again, viewWillLayoutSubviews is an excellent candidate for doing that, for two main reasons:

Order of events

By the time layout occurs, all the other initial conditions are already in place. For example, if the trait collection needed to change, it has changed. If our main view needed to be resized, it has been resized.

Animation

Rotation of the interface is animated. If the layout is changing in response to rotation, we want the change to be animated in coordination with the rotation animation. If we perform our changes in a layoutSubviews event, it will be animated during rotation. This, in effect, is because the runtime has animated a call to layoutIfNeeded before the rotation starts (“Animation and Layout”).

For example, let’s say I have a large green rectangle that should occupy the left one-third of the interface, but only when we are in landscape orientation (the main view’s width is larger than its height) and only when we are in a .regular horizontal size class. This rectangle should come and go in a smooth animated fashion, of course; let’s decide to have it appear from, or vanish off to, the left of the interface.

I can manage all of that entirely by means of manual layout, in a manner exactly parallel to the preceding example:

var greenView : UIView!
override func viewDidLoad() {
    super.viewDidLoad()
    self.greenView = UIView()
    self.greenView.backgroundColor = .green
    self.view.addSubview(self.greenView)
}
override func viewWillLayoutSubviews() {
    func greenViewShouldAppear() -> Bool {
        let tc = self.traitCollection
        let sz = self.view.bounds.size
        if tc.horizontalSizeClass == .regular {
            if sz.width > sz.height {
                return true
            }
        }
        return false
    }
    if greenViewShouldAppear() {
        self.greenView.frame = CGRect(
            0, 0,
            self.view.bounds.width/3.0, self.view.bounds.height
        )
    } else {
        self.greenView.frame = CGRect(
            -self.view.bounds.width/3.0, 0,
            self.view.bounds.width/3.0, self.view.bounds.height
        )
    }
}
Tip

If a view is under the influence of autolayout and we change its constraints instead of its frame in viewWillLayoutSubviews, the change in layout is still animated in coordination with a rotation animation!

Presented View Controller

Back when the only iOS device was an iPhone, a presented view controller was called a modal view controller. When a modal view controller was presented, the root view controller remained in place, but its view was taken out of the interface and the modal view controller’s view was used instead. This was the simplest way to replace the entire interface with a different interface.

You can see why this configuration is characterized as “modal.” The presented view controller’s view has, in a sense, blocked access to the “real” view, the root view controller’s view. The user is forced to work in the presented view controller’s view until the modal view controller is dismissed — its view is removed and the “real” view is visible again — similar to a modal dialog in a desktop application, where the user can’t do anything else but work in the dialog as long as it is present. A presented view controller’s view often reinforces this analogy with obvious dismissal buttons with titles like Save, Done, or Cancel.

The color picker view in my Zotz! app is a good example (Figure 6-8); this is an interface that says, “You are now configuring a color, and that’s all you can do; change the color or cancel, or you’ll be stuck here forever.” The user can’t get out of this view without tapping Cancel or Done, and the view that the user was previously using is visible as a blur behind this view, waiting for the user to return to it.

pios 1907b
Figure 6-8. A modal view

Figure 6-5, from my Latin flashcard app, is another example of a presented view. It has a Cancel button, and the user is in a special “mode,” performing a drill exercise rather than scrolling through flashcards.

Nevertheless, the “modal” characterization is not always apt. A presented view controller might be no more than a technique that you, the programmer, have used to alter the interface; it might not feel “modal” at all. A presented view controller’s view may have a complex interface; it may have child view controllers; it may present yet another view controller; it may take over the interface permanently, with the user never returning to the interface that it replaced. Furthermore, the range of ways in which a presented view controller’s view can be displayed now goes far beyond merely replacing the root view controller’s view. For example:

  • Instead of replacing the entire interface, a presented view controller’s view can replace a subview within the existing interface.

  • A presented view controller’s view may cover the existing interface completely or partially, and without removing any existing interface.

Presentation and Dismissal

The two key methods for presenting and dismissing a view controller are:

present(_:animated:completion:)

To make a view controller present another view controller, you send the first view controller this message, handing it the second view controller, which you will probably instantiate for this very purpose. The first view controller is very typically self.

We now have two view controllers that stand in the relationship of being one another’s presentingViewController and presentedViewController respectively. The presented view controller is retained, and its view replaces or covers the presenting view controller’s view in the interface.

dismiss(animated:completion:)

The “presented” state of affairs described in the previous paragraph persists until the presenting view controller is sent this message. The presented view controller’s view is then removed from the interface, the original interface is restored, and the presented view controller is released; it will thereupon typically go out of existence.

As the view of the presented view controller appears, and again when it is dismissed, there’s an option for animation to be performed as the transition takes place (the animated: argument, a Bool). The completion: parameter, which can be nil, lets you supply a function to be run after the transition (including the animation) has occurred.

View controller relationships during presentation

The presenting view controller (the presented view controller’s presentingViewController) is not necessarily the same view controller to which you sent present(_:animated:completion:). It will help if we distinguish three roles that view controllers can play in presenting a view controller:

Presented view controller

The first argument to present(_:animated:completion:).

Original presenter

The view controller to which present(_:animated:completion:) was sent. Apple sometimes refers to this view controller as the source; “original presenter” is my own term.

The presented view controller is set as the original presenter’s presentedViewController.

Presenting view controller

The view controller whose view is replaced or covered by the presented view controller’s view. By default, it is the view controller that was the top-level view controller prior to the presentation. It might not be the same as the original presenter.

This view controller is set as the presented view controller’s presentingViewController. The presented view controller is set as the presenting view controller’s presentedViewController. (Thus, the presented view controller might be the presentedViewController of two different view controllers.)

The receiver of dismiss(animated:completion:) may be any of those three objects; the runtime will use the linkages between them to transmit the necessary messages up the chain on your behalf to the presentingViewController.

You can test whether a view controller’s presentedViewController or presentingViewController is nil to learn whether presentation is occurring. For example, a view controller whose presentingViewController is nil is not a presented view controller at this moment.

A view controller can have at most one presentedViewController. If you send present(_:animated:completion:) to a view controller whose presentedViewController isn’t nil, nothing will happen and the completion function is not called (and you’ll get a warning from the runtime). However, a presented view controller can itself present a view controller, so there can be a chain of presented view controllers.

If you send dismiss(animated:completion:) to a view controller in the middle of a presentation chain — a view controller that has both a presentingViewController and a presentedViewController — then its presentedViewController is dismissed.

If you send dismiss(animated:completion:) to a view controller whose presentedViewController is nil and that has no presentingViewController, nothing will happen (not even a warning in the console), and the completion function is not called.

Manual view controller presentation

Let’s make one view controller present another. We could do this simply by connecting one view controller to another in a storyboard with a modal segue, but I don’t want you to do that: a modal segue calls present(_:animated:completion:) for you, whereas I want you to call it yourself.

So start with an iPhone project made from the Single View App template. This contains one view controller class, called ViewController. Our first move must be to add a second view controller class, an instance of which will function as the presented view controller:

  1. Choose File → New → File and specify iOS → Source → Cocoa Touch Class. Click Next.

  2. Name the class SecondViewController, make sure it is a subclass of UIViewController, and check the XIB checkbox so that we can design this view controller’s view quickly and easily in the nib editor. Click Next.

  3. Confirm the folder, group, and app target membership, and click Create.

  4. Edit SecondViewController.xib, and do something there to make the view distinctive, so that you’ll recognize it when it appears; for example, give it a red background color.

  5. In ViewController.swift, give ViewController an action method that instantiates SecondViewController and presents it:

    @IBAction func doPresent(_ sender: Any?) {
        let svc = SecondViewController(nibName: nil, bundle: nil)
        self.present(svc, animated:true)
    }
  6. Edit Main.storyboard and add a button to the ViewController’s main view. Connect that button to ViewController’s doPresent.

Run the project. In ViewController’s view, tap the button. SecondViewController’s view slides into place over ViewController’s view.

In our lust for instant gratification, we have neglected to provide a way to dismiss the presented view controller. If you’d like to do that:

  1. In SecondViewController.swift, give SecondViewController an action method that dismisses SecondViewController:

    @IBAction func doDismiss(_ sender: Any?) {
        self.presentingViewController?.dismiss(animated:true)
    }
  2. Edit SecondViewController.xib and add a button to SecondViewController’s view. Connect that button to SecondViewController’s doDismiss.

Run the project. You can now alternate between ViewController’s view and SecondViewController’s view, presenting and dismissing in turn. Go ahead and play for a while with your exciting new app; I’ll wait.

Configuring a Presentation

This section describes some configurable aspects of how a view controller’s view behaves as the view controller is presented.

Transition style

When a view controller is presented and later when it is dismissed, a simple animation of its view can be performed, according to whether the animated: parameter of the corresponding method is true. There are a few different built-in animation types (modal transition styles) to choose from.

Note

Instead of choosing a simple built-in modal transition style, you can supply your own animation, as I’ll explain later in this chapter.

To choose a built-in animation, set the presented view controller’s modalTransitionStyle property prior to the presentation. This value can be set in code or in the nib editor. Your choices (UIModalTransitionStyle) are:

.coverVertical (the default)

The view slides up from the bottom to cover the presenting view controller’s view on presentation and down to reveal it on dismissal. The definition of “bottom” depends on the orientation of the device and the orientations the view controllers support.

.flipHorizontal

The view flips on the vertical axis as if the two views were the front and back of a piece of paper. The “vertical axis” is the device’s long axis, regardless of the app’s orientation.

This transition style provides one of those rare occasions where the user may directly glimpse the window behind the transitioning views. You may want to set the window’s background color appropriately.

.crossDissolve

The views remain stationary, and one fades into the other.

.partialCurl

The presenting view controller’s view curls up like a page in a notepad to reveal the presented view controller’s view. In iOS 7 and before, a drawing of a curl covers the top left of the presented view; tapping it dismisses the presented view controller. In iOS 8 and later, the curl drawing is missing, but tapping where it should be dismisses the presented view controller anyway! I regard that as a bug; I have an elaborate workaround, but the simplest solution is to avoid .partialCurl entirely.

Presentation style

By default, the presented view controller’s view occupies the entire screen, completely replacing that of the presenting view controller. But you can choose from some other built-in options expressing how the presented view controller’s view should cover the screen (modal presentation styles).

Note

Instead of choosing a simple built-in modal presentation style, you can customize the presentation to place the presented view controller’s view anywhere you like, as I’ll explain later in this chapter.

To choose a presentation style, set the presented view controller’s modalPresentationStyle property prior to the presentation. This value can be set in code or in the nib editor. Your choices (UIModalPresentationStyle) are:

.fullScreen

The default. The presenting view controller is the top-level view controller, and its view — meaning the entire interface — is replaced.

.overFullScreen

Similar to .fullScreen, but the presenting view controller’s view is not replaced; instead, it stays where it is, possibly being visible during the transition, and remaining visible behind the presented view controller’s view if the latter has some transparency.

.pageSheet

Similar to .fullScreen, but in portrait orientation on the iPad it’s a little shorter (leaving a gap behind the status bar), and in landscape orientation on the iPad or a big iPhone it’s also narrower, with the presenting view controller’s view remaining partially visible (and dimmed) behind it. Treated as .fullScreen on the iPhone (including a big iPhone in portrait).

.formSheet

Similar to .pageSheet, but on the iPad it’s even smaller, allowing the user to see more of the presenting view controller’s view behind it. As the name implies, this is intended to allow the user to fill out a form (Apple describes this as “gathering structured information from the user”). On a big iPhone in landscape, indistinguishable from .pageSheet. Treated as .fullScreen on the iPhone (including a big iPhone in portrait).

A .formSheet presented view controller, even on an iPad, has a .compact horizontal size class.

.currentContext

The presenting view controller can be any view controller, such as a child view controller. The presented view controller’s view replaces the presenting view controller’s view, which may have been occupying only a portion of the screen. I’ll explain in a moment how to specify the presenting view controller.

.overCurrentContext

Like .currentContext, but the presented view controller’s view covers the presenting view controller’s view rather than replacing it. Again, this may mean that the presented view controller’s view now covers only a portion of the screen. .overCurrentContext will often be a better choice than .currentContext, because some subviews don’t behave well when automatically removed from their superview and restored later.

Current context presentation

When the presented view controller’s modalPresentationStyle is .currentContext or .overCurrentContext, a decision has to be made by the runtime as to what view controller should be the presenting view controller. This will determine what view will be replaced or covered by the presented view controller’s view. The decision involves another UIViewController property, definesPresentationContext (a Bool), and possibly still another UIViewController property, providesPresentationContextTransitionStyle. Here’s how the decision operates:

  1. Starting with the original presenter (the view controller to which present(_:animated:completion:) was sent), we walk up the chain of parent view controllers, looking for one whose definesPresentationContext property is true. If we find one, that’s the one; it will be the presentingViewController, and its view will be replaced or covered by the presented view controller’s view.

    (If we don’t find one, things work as if the presented view controller’s modalPresentationStyle had been .fullScreen.)

  2. If, during the search just described, we find a view controller whose definesPresentationContext property is true, we look to see if that view controller’s providesPresentationContextTransitionStyle property is also true. If so, that view controller’s modalTransitionStyle is used for this transition animation, rather than the presented view controller’s modalTransitionStyle.

To illustrate, I need a parent–child view controller arrangement to work with. This chapter hasn’t yet discussed any parent view controllers in detail, but the simplest is UITabBarController, which I discuss in the next section, and it’s easy to create a working app with a UITabBarController-based interface, so that’s the example I’ll use:

  1. Start with the Tabbed App template. It provides three view controllers — the UITabBarController and two children, FirstViewController and SecondViewController.

  2. As in the previous example, I want us to create and present the presented view controller manually, rather than letting the storyboard do it automatically; so make a new view controller class with an accompanying .xib file, to use as a presented view controller — call it ExtraViewController.

  3. In ExtraViewController.xib, give the view a distinctive background color, so you’ll recognize it when it appears.

  4. In the storyboard, put a button in the First View Controller view (First Scene), and connect it to an action method in FirstViewController.swift that summons the new view controller as a presented view controller:

    @IBAction func doPresent(_ sender: Any?) {
        let vc = ExtraViewController(nibName: nil, bundle: nil)
        vc.modalTransitionStyle = .flipHorizontal
        self.present(vc, animated: true)
    }

Run the project and tap the button. Observe that the presented view controller’s view occupies the entire interface, covering even the tab bar; it replaces the root view, because the presentation style is .fullScreen. The presenting view controller is the root view controller, which is the UITabBarController.

Now change the code to look like this:

@IBAction func doPresent(_ sender: Any?) {
    let vc = ExtraViewController(nibName: nil, bundle: nil)
    vc.modalTransitionStyle = .flipHorizontal
    self.definesPresentationContext = true // *
    vc.modalPresentationStyle = .currentContext // *
    self.present(vc, animated: true)
}

Run the project and tap the button. The presented view controller’s view replaces only the first view controller’s view; the tab bar remains visible. That’s because the presented view controller’s modalPresentationStyle is .currentContext, and definesPresentationContext is true in FirstViewController. Thus the search for a context stops in FirstViewController, which becomes the presenting view controller — meaning that the presented view replaces FirstViewController’s view instead of the root view.

We can also override the presented view controller’s transition animation through the modalTransitionStyle property of the presenting view controller:

@IBAction func doPresent(_ sender: Any?) {
    let vc = ExtraViewController(nibName: nil, bundle: nil)
    vc.modalTransitionStyle = .flipHorizontal
    self.definesPresentationContext = true
    self.providesPresentationContextTransitionStyle = true // *
    self.modalTransitionStyle = .coverVertical // *
    vc.modalPresentationStyle = .currentContext
    self.present(vc, animated: true)
}

Because the presenting view controller’s providesPresentationContextTransitionStyle is true, the transition uses the .coverVertical animation belonging to the presenting view controller, rather than the .flipHorizontal animation of the presented view controller.

Configuration in the nib editor

Most of what I’ve described so far can be configured in a .storyboard or .xib file. A view controller’s Attributes inspector lets you set its transition style and presentation style, as well as definesPresentationContext and providesPresentationContextTransitionStyle.

If you’re using a storyboard, you can configure one view controller to present another view controller by connecting them with a Present Modally segue; to do the presentation, you trigger the segue (or give the user a way to trigger it) instead of calling present(_:animated:completion:). The segue’s Attributes inspector lets you set the presentation style and transition style (and whether there is to be animation). Dismissal is a little more involved; either you must dismiss the presented view controller in code, by calling dismiss(animated:completion:), or you must use an unwind segue. I’ll discuss triggered segues and unwind segues in detail later in this chapter.

Communication with a Presented View Controller

In real life, it is highly probable that the original presenter will have additional information to impart to the presented view controller as the latter is created and presented, and that the presented view controller will want to pass information back to the original presenter as it is dismissed. Knowing how to arrange this exchange of information is very important.

Passing information from the original presenter to the presented view controller is usually easy, because the original presenter typically has a reference to the presented view controller before the latter’s view appears in the interface. For example, suppose the presented view controller has a public data property. Then the original presenter can easily set this property, especially if the original presenter is the one instantiating the presented view controller in the first place:

@IBAction func doPresent(_ sender: Any?) {
    let svc = SecondViewController(nibName: nil, bundle: nil)
    svc.data = "This is very important data!" // *
    self.present(svc, animated:true)
}

Indeed, if you’re instantiating the presented view controller in code, as we are here, you might even give its class a designated initializer that accepts — and thus requires — this data. In my Latin vocabulary app, for example, I’ve given DrillViewController a designated initializer init(terms:) precisely so that whoever creates it must pass it the data it will need to do its job while it exists.

Passing information back from the presented view controller to the original presenter is a more interesting problem. The presented view controller will need to know who the original presenter is, but it doesn’t automatically have a reference to it (the original presenter, remember, is not necessarily the same as the presentingViewController). Moreover, the presented view controller will need to know the signature of some method, implemented by the original presenter, that it can call in order to hand over the information — and this needs to work regardless of the original presenter’s class.

The standard solution is to use delegation, as follows:

  1. The presented view controller defines a protocol declaring a method that the presented view controller wants to call before it is dismissed.

  2. The original presenter conforms to this protocol: it declares adoption of the protocol, and it implements the required method.

  3. The presented view controller provides a means whereby it can be handed a reference to an object conforming to this protocol. Think of that reference as the presented view controller’s delegate. Very often, this will be a property — perhaps called delegate — typed as the protocol. (Such a property should probably be weak, since an object usually has no business retaining its delegate.)

  4. As the original presenter creates and configures the presented view controller, it hands the presented view controller a reference to itself, in its role as adopter of the protocol, by assigning itself as the delegate of the presented view controller.

This sounds elaborate, but with practice you’ll find yourself able to implement it very quickly. And you can see why it works: because its delegate is typed as the protocol, the presented view controller is guaranteed that its delegate, if it exists, implements the method declared in the protocol. Thus, the desired communication from the presented view controller to whoever configured and created it is assured.

To illustrate this architecture, suppose that (as in our earlier example) the root view controller, ViewController, presents SecondViewController. Then our code in SecondViewController.swift would look like this:

protocol SecondViewControllerDelegate : class {
    func accept(data:Any!)
}
class SecondViewController : UIViewController {
    var data : Any?
    weak var delegate : SecondViewControllerDelegate?
    @IBAction func doDismiss(_ sender: Any?) {
        self.delegate?.accept(data:"Even more important data!")
    }
}

It is now ViewController’s job to adopt the SecondViewControllerDelegate protocol, and to set itself as the SecondViewController’s delegate. If it does so, then when the delegate method is called, ViewController will be handed the data, and it should then dismiss the SecondViewController:

class ViewController : UIViewController, SecondViewControllerDelegate {
    @IBAction func doPresent(_ sender: Any?) {
        let svc = SecondViewController(nibName: nil, bundle: nil)
        svc.data = "This is very important data!"
        svc.delegate = self // *
        self.present(svc, animated:true)
    }
    func accept(data:Any!) {
        // do something with data here
        self.dismiss(animated:true)
    }
}

That is a perfectly satisfactory implementation, and we could stop at this point. One might object, however, that too much responsibility now rests upon the original presenter (the delegate): it is sent the data and then it must also dismiss the presented view controller. Perhaps the presented view controller should hand back any data and should then dismiss itself (as it did in my earlier example). Even better, the presented view controller should hand back any data automatically, regardless of how it is dismissed.

We can arrange that by putting all the responsibility on the presented view controller. In the preceding example, we delete the self.dismiss call from ViewController’s accept(data:); in SecondViewController, we will implement both the task of dismissal and the task of handing back the data, separately.

To make the latter task automatic, SecondViewController can arrange to hear about its own dismissal by implementing viewWillDisappear (discussed later in this chapter), which will then call accept(data:) to ensure that the data is handed across. There is more than one reason why viewWillDisappear might be called; we can ensure that this really is the moment of our own dismissal by consulting isBeingDismissed. Here’s how SecondViewController would look now:

protocol SecondViewControllerDelegate : class {
    func accept(data:Any!)
}
class SecondViewController : UIViewController {
    var data : Any?
    weak var delegate : SecondViewControllerDelegate?
    @IBAction func doDismiss(_ sender: Any?) {
        self.presentingViewController?.dismiss(animated:true)
    }
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        if self.isBeingDismissed {
            self.delegate?.accept(data:"Even more important data!")
        }
    }
}

If you’re using a storyboard and a Present Modally segue, things are a bit different. You don’t have access to the presented view controller at the moment of creation, and the storyboard will not automatically call a custom initializer on the presented view controller. Instead, in the original presenter (the source of the segue), you typically implement prepare(for:sender:) as a moment when the original presenter and the presented view controller will meet, and the former can hand across any needed data, set itself as delegate, and so forth. If you dismiss the presented view controller automatically by way of an unwind segue, the same is true in reverse: the presented view controller also implements prepare(for:sender:), and this is called on dismissal — so that’s when the presented view controller calls the delegate method. I’ll give more details later in this chapter.

Adaptive Presentation

When a view controller with a modalPresentationStyle of .pageSheet or .formSheet is about to appear, you get a second opportunity to change its effective modalPresentationStyle, and even to substitute a different view controller, based on the current trait collection environment. This is called adaptive presentation. The idea is that your presented view controller might appear one way for certain trait collections and another way for others — for example, on an iPad as opposed to an iPhone.

To implement adaptive presentation, you use a view controller’s presentation controller (presentationController, a UIPresentationController). Before presenting a view controller, you set its presentation controller’s delegate to an object adopting the UIAdaptivePresentationControllerDelegate protocol. Before the presented view controller’s view appears, the delegate is sent these messages:

adaptivePresentationStyle(for:traitCollection:)

The first parameter is the presentation controller, and its presentationStyle is the modalPresentationStyle it proposes to use. Return a different modal presentation style to use instead (or .none if you don’t want to change the presentation style).

presentationController(_:willPresentWithAdaptiveStyle:transitionCoordinator:)

Called just before the presentation takes place. If the adaptiveStyle: is .none, adaptive presentation is not going to take place.

presentationController(_:viewControllerForAdaptivePresentationStyle:)

Called only if adaptive presentation is going to take place. The first parameter is the presentation controller, and its presentedViewController is the view controller it proposes to present. Return a different view controller to present instead (or nil to keep the current presented view controller).

What adaptations are you permitted to perform? First, as I’ve already said, the original modalPresentationStyle should be .pageSheet or .formSheet. It isn’t illegal to try to adapt from other presentation styles, but it isn’t going to work either. Then the possibilities are as follows:

Adapt sheet to full screen

You can adapt .pageSheet or .formSheet to .fullScreen or .overFullScreen.

Adapt page sheet to form sheet

You can adapt .pageSheet to .formSheet. On an iPad, the difference is clearly visible, but on a big iPhone in landscape, the two sheet types are indistinguishable. On an iPhone (including a big iPhone in portrait), the result is something a little unusual and unexpected. It’s similar to .pageSheet on the iPad in portrait orientation: it is full width, but a little shorter than full height, leaving a gap behind the status bar. (You can also obtain this configuration by adapting .pageSheet or .formSheet to .none.)

For example, here’s how to present a view controller as a .pageSheet on iPad but as .overFullScreen on iPhone:

extension ViewController : UIAdaptivePresentationControllerDelegate {
    @IBAction func doPresent(_ sender: Any?) {
        let svc = SecondViewController(nibName: nil, bundle: nil)
        svc.modalPresentationStyle = .pageSheet
        svc.presentationController!.delegate = self // *
        self.present(svc, animated:true)
    }
    func adaptivePresentationStyle(for controller: UIPresentationController,
        traitCollection: UITraitCollection) -> UIModalPresentationStyle {
            if traitCollection.horizontalSizeClass == .compact {
                return .overFullScreen
            }
            return .none // don't adapt
    }
}

Now let’s extend that example by presenting one view controller on iPad but a different view controller on iPhone; this method won’t be called when adaptivePresentationStyle returns .none, so it affects iPhone only:

extension ViewController : UIAdaptivePresentationControllerDelegate {
    func presentationController(_ controller: UIPresentationController,
        viewControllerForAdaptivePresentationStyle: UIModalPresentationStyle)
        -> UIViewController? {
            let newvc = ThirdViewController(nibName: nil, bundle: nil)
            return newvc
    }
}

In real life, of course, when substituting a different view controller, you might need to prepare it before returning it (for example, giving it data and setting its delegate). A common scenario is to return the same view controller wrapped in a navigation controller; I’ll illustrate in Chapter 9.

Presentation, Rotation, and the Status Bar

A fullscreen presented view controller effectively replaces your app’s entire interface. Thus, even though it is not the root view controller, it is the top-level view controller, and acquires the same mighty powers as if it were the root view controller. In particular:

  • Its supportedInterfaceOrientations and shouldAutorotate are consulted and honored; this view controller gets to limit your app’s legal orientations.

  • Its prefersStatusBarHidden and preferredStatusBarStyle are consulted and honored; this view controller gets to dictate the appearance of the status bar.

If a fullscreen presented view controller’s supportedInterfaceOrientations do not intersect with the app’s current orientation, the app’s orientation will rotate, as the presented view appears, to an orientation that the presented view controller supports — and the same thing will be true in reverse when the presented view controller is dismissed. Thus, a presented view controller allows you to force the interface to rotate — in other words, to perform forced rotation, as I called it earlier in this chapter. In fact, a presented view controller is the only officially sanctioned way to perform forced rotation.

Warning

On an iPad, if your app permits all four orientations and does not opt out of iPad multitasking, its view controllers’ supportedInterfaceOrientations are not even consulted, so forced rotation doesn’t work.

The presented view controller’s supportedInterfaceOrientations bitmask might permit multiple possible orientations. The view controller may then also wish to specify which of those multiple orientations it should have initially when it is presented. To do so, override preferredInterfaceOrientationForPresentation; this property is consulted before supportedInterfaceOrientations, and its value is a single UIInterfaceOrientation (not a bitmask).

Warning

Do not attempt to implement forced rotation in a presented view controller whose presentation style is not .fullScreen. Such a configuration is not supported, and very weird things may happen.

When a view controller is presented, if its presentation style is not .fullScreen, a question arises of whether its status bar properties (prefersStatusBarHidden and preferredStatusBarStyle) should be consulted. By default, the answer is no, because this view controller is not the top-level view controller. To make the answer be yes, set this view controller’s modalPresentationCapturesStatusBarAppearance to true.

Tab Bar Controller

A tab bar (UITabBar, see also Chapter 12) is a horizontal bar containing items. Each item is a UITabBarItem; it displays, by default, an image and a title. The title usually appears beside the image; however, on an iPhone in portrait orientation, the title appears below the image. At all times, exactly one of a tab bar’s items is selected (highlighted); when the user taps an item, it becomes the selected item.

If there are too many items to fit on a tab bar, the excess items are automatically subsumed into a final More item. When the user taps the More item, a list of the excess items appears, and the user can select one; the user can also be permitted to edit the tab bar, determining which items appear in the tab bar itself and which ones spill over into the More list.

A tab bar is an independent interface object, but it is most commonly used in conjunction with a tab bar controller (UITabBarController, a subclass of UIViewController) to form a tab bar interface. The tab bar controller displays the tab bar at the bottom of its own view. From the user’s standpoint, the tab bar items correspond to views; when the user selects a tab bar item, the corresponding view appears, filling the remainder of the space. The user is thus employing the tab bar to choose an entire area of your app’s functionality. In reality, the UITabBarController is a parent view controller; you give it child view controllers, which the tab bar controller then contains, and the views summoned by tapping the tab bar items are the views of those child view controllers.

Familiar examples of a tab bar interface on the iPhone are Apple’s Clock app and Music app.

A tab bar interface can automatically change the height of its tab bar. By default, the tab bar height is 49 points; but when the vertical size class is .compact (an iPhone in landscape orientation, except for a big iPhone), the tab bar height is reduced to 32, to provide more room to display the main interface.

You can get a reference to the tab bar controller’s tab bar through its tabBar property. In general, you won’t need this. When using a tab bar interface by way of a UITabBarController, you do not interact (as a programmer) with the tab bar itself; you don’t create it or set its delegate. You provide the UITabBarController with children, and it does the rest; when the UITabBarController’s view is displayed, there’s the tab bar along with the view of the selected item. You can, however, customize the look of the tab bar (see Chapter 12 for details).

Tab Bar Items

For each view controller you assign as a tab bar controller’s child, you’re going to need a tab bar item, which will appear as its representative in the tab bar. This tab bar item will be your child view controller’s tabBarItem. A tab bar item is a UITabBarItem; this is a subclass of UIBarItem, an abstract class that provides some of its most important properties, such as title, image, and isEnabled.

There are two ways to make a tab bar item:

By borrowing it from the system

Instantiate UITabBarItem using init(tabBarSystemItem:tag:), and assign the instance to your child view controller’s tabBarItem. Consult the documentation for the list of available system items. Unfortunately, you can’t customize a system tab bar item’s title; you must accept the title the system hands you. (You can’t work around this restriction by somehow copying a system tab bar item’s image.)

By making your own

Instantiate UITabBarItem using init(title:image:tag:) and assign the instance to your child view controller’s tabBarItem. Alternatively, use the view controller’s existing tabBarItem and set its image and title. Instead of setting the title of the tabBarItem, you can set the title property of the view controller itself; doing this automatically sets the title of its current tabBarItem (unless the tab bar item is a system tab bar item), though the converse is not true.

You can add a separate selectedImage later, or by initializing with init(title:image:selectedImage:). The selectedImage will be displayed in place of the normal image when this tab bar item is selected in the tab bar.

The image (and selectedImage) for a tab bar item should be a 30×30 PNG, or a PDF vector image (see Chapter 2). By default, it will be treated as a transparency mask (a template): the hue of its pixels will be ignored, and the transparency of its pixels will be combined with the tab bar’s tintColor, which may be inherited from higher up the view hierarchy. However, you can instead display the image as is, and not as a transparency mask, by deriving an image whose rendering mode is .alwaysOriginal.

If the image is not a PDF vector image, you will have to cope with the possibility that the tab bar item will be displayed at reduced size when the tab bar’s height is reduced. The solution is to set the tab bar item’s landscapeImagePhone to a 20×20 PNG; it will be used when the vertical size class is .compact — exactly the circumstances under which the tab bar’s height is reduced.

Warning

A tab bar item’s selectedImage is not used in a .compact vertical size class environment if it also has a separate landscapeImagePhone. I regard this as a bug.

Other ways in which you can customize the look of a tab bar item are discussed in Chapter 12.

Configuring a Tab Bar Controller

Basic configuration of a tab bar controller is very simple: just hand it the view controllers that will be its children. To do so, collect those view controllers into an array and set the UITabBarController’s viewControllers property to that array. The view controllers in the array are now the tab bar controller’s child view controllers; the tab bar controller is the parent of the view controllers in the array. The tab bar controller is also the tabBarController of the view controllers in the array and of all their children; thus a child view controller at any depth can learn that it is contained by a tab bar controller and can get a reference to that tab bar controller. The tab bar controller retains the array, and the array retains the child view controllers.

Here’s a simple example from one of my apps, in which I construct and display a tab bar interface in code:

func application(_ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions:
    [UIApplication.LaunchOptionsKey : Any]?) -> Bool {
        self.window = self.window ?? UIWindow()
        let vc1 = GameBoardController()
        let sc = SettingsController()
        let vc2 = UINavigationController(rootViewController:sc)
        let tabBarController = UITabBarController()
        tabBarController.viewControllers = [vc1, vc2]
        tabBarController.selectedIndex = 0
        tabBarController.delegate = self
        self.window!.rootViewController = tabBarController
        self.window!.makeKeyAndVisible()
        return true
}

The tab bar controller’s tab bar will automatically display the tabBarItem of each child view controller. The order of the tab bar items is the order of the view controllers in the tab bar controller’s viewControllers array. Thus, a child view controller will probably want to configure its tabBarItem property early in its lifetime, so that the tabBarItem is ready by the time the view controller is handed as a child to the tab bar controller. Observe that viewDidLoad is not early enough! That’s because the view controllers (other than the initially selected view controller) have no view when the tab bar controller initially appears. Thus it is common to implement an initializer for this purpose.

Here’s an example from the same app as the previous code (in the GameBoardController class):

init() {
    super.init(nibName:nil, bundle:nil)
    // tab bar configuration
    self.tabBarItem.image = UIImage(named: "game")
    self.title = "Game"
}

If you change the tab bar controller’s view controllers array later in its lifetime and you want the corresponding change in the tab bar’s display of its items to be animated, call setViewControllers(_:animated:).

To ask the tab bar controller which tab bar item the user has selected, you can couch your query in terms of the child view controller (selectedViewController) or by index number in the array (selectedIndex). You can also set those properties to switch between displayed child view controllers programmatically. If you don’t do that before the tab bar controller appears, then initially the first tab bar item will be selected.

Note

You can supply a view animation when a tab bar controller’s selected tab item changes and one child view controller’s view is replaced by another, as I’ll explain later in this chapter.

You can also set the UITabBarController’s delegate (adopting UITabBarControllerDelegate). The delegate gets messages allowing it to prevent a given tab bar item from being selected, and notifying it when a tab bar item is selected and when the user is customizing the tab bar from the More item.

If a tab bar controller is the top-level view controller, it determines your app’s compensatory rotation behavior. To take a hand in that determination without having to subclass UITabBarController, make one of your objects the tab bar controller’s delegate and implement these methods, as needed:

  • tabBarControllerSupportedInterfaceOrientations(_:)

  • tabBarControllerPreferredInterfaceOrientationForPresentation(_:)

A top-level tab bar controller also determines your app’s status bar appearance. However, a tab bar controller implements childForStatusBarStyle and childForStatusBarHidden so that the actual decision is relegated to the child view controller whose view is currently being displayed: your preferredStatusBarStyle and prefersStatusBarHidden are consulted and obeyed.

If the tab bar contains few enough items that it doesn’t need a More item, there won’t be one, and the tab bar won’t be user-customizable. If there is a More item, you can exclude some tab bar items from being customizable by setting the customizableViewControllers property to an array that lacks them; setting this property to nil means that the user can see the More list but can’t rearrange the items. Setting the viewControllers property sets the customizableViewControllers property to the same value, so if you’re going to set the customizableViewControllers property, do it after setting the viewControllers property. The moreNavigationController property can be compared with the selectedViewController property to learn whether the user is currently viewing the More list; apart from this, the More interface is mostly out of your control, but I’ll discuss some sneaky ways of customizing it in Chapter 12.

(If you allow the user to rearrange items, you would presumably want to save the new arrangement and restore it the next time the app runs. You might use UserDefaults for this, or you might take advantage of the built-in automatic state saving and restoration facilities, discussed later in this chapter.)

You can configure a UITabBarController in a storyboard. The UITabBarController’s contained view controllers can be set directly — there will be a “view controllers” relationship between the tab bar controller and each of its children — and the contained view controllers will be instantiated together with the tab bar controller. Moreover, each contained view controller has a Tab Bar Item; you can select this and set many aspects of the tabBarItem, such as its system item or its title, image, selected image, and tag, directly in the nib editor. (If a view controller in a nib doesn’t have a Tab Bar Item and you want to configure this view controller for use in a tab bar interface, drag a Tab Bar Item from the Library onto the view controller.)

To start a project with a main storyboard that has a UITabBarController as its initial view controller, begin with the Tabbed App template.

Navigation Controller

A navigation bar (UINavigationBar, see also Chapter 12) is a horizontal bar displaying a center title and a right button. When the user taps the right button, the navigation bar animates, sliding its interface out to the left and replacing it with a new interface that enters from the right. The new interface displays a back button at the left side, and a new center title — and possibly a new right button. The user can tap the back button to go back to the first interface, which slides in from the left; or, if there’s a right button in the second interface, the user can tap it to go further forward to a third interface, which slides in from the right.

The successive interfaces of a navigation bar thus behave like a stack. In fact, a navigation bar does represent an actual stack — an internal stack of navigation items (UINavigationItem). It starts out with one navigation item: the root item or bottom item of the stack. Since there is initially just one navigation item, it is also initially the top item of the stack (the navigation bar’s topItem). The navigation bar’s interface is always representing whatever its top item is at that moment. When the user taps a right button, a new navigation item is pushed onto the stack; it becomes the top item, and its interface is seen. When the user taps a back button, the top item is popped off the stack, and what was previously the next item beneath it in the stack — the back item (the navigation bar’s backItem) — becomes the top item, and its interface is seen.

The state of the stack is thus reflected in the navigation bar’s interface. The navigation bar’s center title comes automatically from the top item, and its back button comes from the back item. (See Chapter 12 for a complete description.) Thus, the title tells the user what item is current, and the left side is a button indicating the item we would return to if the user were to tap that button. The animations reinforce this notion of directionality, giving the user a sense of position within a chain of items.

A navigation bar is an independent interface object, but it is most commonly used in conjunction with a navigation controller (UINavigationController, a subclass of UIViewController) to form a navigation interface. Just as there is a stack of navigation items in the navigation bar, there is a stack of view controllers in the navigation controller. These view controllers are the navigation controller’s children, and each navigation item belongs to a view controller — it is a view controller’s navigationItem.

The navigation controller performs automatic coordination of the navigation bar and the overall interface. Whenever a view controller comes to the top of the navigation controller’s stack, its view is displayed in the interface. At the same time, its navigationItem is automatically pushed onto the top of the navigation bar’s stack — and is thus displayed in the navigation bar as its top item. Moreover, the animation in the navigation bar is reinforced by animation of the interface as a whole: by default, a view controller’s view slides into the main interface from the side just as its navigation item slides into the navigation bar from the same side.

Note

You can supply a different view animation when a view controller is pushed onto or popped off a navigation controller’s stack, as I’ll explain later in this chapter.

Your code can control the overall navigation, so in real life the user might push a new view controller, not by tapping the right button in the navigation bar, but by tapping something inside the main interface, such as a listing in a table view. (Figure 6-1 is a navigation interface that works this way.) In this situation, your app is deciding in real time, in response to the user’s tap, what the next view controller should be; typically, you won’t even create the next view controller until the user asks to navigate to it. The navigation interface thus becomes a master–detail interface.

Conversely, you might put a view controller inside a navigation controller just to get the convenience of the navigation bar, with its title and buttons, even when no actual navigation is going to take place.

You can get a reference to the navigation controller’s navigation bar through its navigationBar property. In general, you won’t need this. When using a navigation interface by way of a UINavigationController, you do not interact (as a programmer) with the navigation bar itself; you don’t create it or set its delegate. You provide the UINavigationController with children, and it does the rest, handing each child view controller’s navigationItem to the navigation bar for display and showing the child view controller’s view each time navigation occurs. You can, however, customize the look of the navigation bar (see Chapter 12 for details).

A navigation interface may also optionally display a toolbar at the bottom. A toolbar (UIToolbar) is a horizontal view displaying a row of items. As in a navigation bar, a toolbar item may provide information, or it may be something the user can tap. A tapped item is not selected, as in a tab bar; rather, it represents the initiation of an action, like a button. You can get a reference to a UINavigationController’s toolbar through its toolbar property. The look of the toolbar can be customized (Chapter 12). In a navigation interface, however, the contents of the toolbar are determined automatically by the view controller that is currently the top item in the stack: they are its toolbarItems.

Note

A UIToolbar can also be used independently, and often is. It then typically appears at the bottom on an iPhone — Figure 6-3 has a toolbar at the bottom — but often appears at the top on an iPad, where it plays something of the role that the menu bar plays on the desktop. When a toolbar is displayed by a navigation controller, though, it always appears at the bottom.

A familiar example of a navigation interface is Apple’s Settings app on the iPhone. The Mail app on the iPhone is a navigation interface that includes a toolbar.

Bar Button Items

The items in a UIToolbar or a UINavigationBar are bar button items — UIBarButtonItem, a subclass of UIBarItem. A bar button item comes in one of two broadly different flavors:

Basic bar button item

The bar button item behaves like a simple button.

Custom view

The bar button item has no inherent behavior, but has (and displays) a customView.

UIBarItem is not a UIView subclass. A basic bar button item is button-like, but it has no frame, no UIView touch handling, and so forth. A UIBarButtonItem’s customView, however, is a UIView — indeed, it can be any kind of UIView. Thus, a bar button item with a customView can display any sort of view in a toolbar or navigation bar, and that view can have subviews, can implement touch handling, and so on.

Let’s start with the basic bar button item (no custom view). A bar button item, like a tab bar item, inherits from UIBarItem the title, image, and isEnabled properties. The title text color, by default, comes from the bar button item’s tintColor, which may be inherited from the bar itself or from higher up the view hierarchy. Assigning an image removes the title. The image should usually be quite small; Apple recommends 22×22. By default, it will be treated as a transparency mask (a template): the hue of its pixels will be ignored, and the transparency of its pixels will be combined with the bar button item’s tintColor. However, you can instead display the image as is, and not as a transparency mask, by deriving an image whose rendering mode is .alwaysOriginal (see Chapter 2).

A basic bar button item has a style property (UIBarButtonItem.Style); this will usually be .plain. The alternative, .done, causes the title to be bold. You can further refine the title font and style. In addition, a bar button item can have a background image; this will typically be a small, resizable image, and can be used to provide a border. Full details appear in Chapter 12.

A bar button item also has target and action properties. These contribute to its button-like behavior: tapping a bar button item can trigger an action method elsewhere.

There are three ways to make a bar button item:

By borrowing it from the system

Make a UIBarButtonItem with init(barButtonSystemItem:target:​action:). Consult the documentation for the list of available system items; they are not the same as for a tab bar item. You can’t assign a title or change the image. (But you can change the tint color or assign a background image.)

By making your own basic bar button item

Make a UIBarButtonItem with init(title:style:target:action:) or with init(image:style:target:action:).

An additional initializer, init(image:landscapeImagePhone:style:target:​action:), lets you specify a second image for use when the vertical size class is .compact, because the bar’s height might be smaller in this situation.

By making a custom view bar button item

Make a UIBarButtonItem with init(customView:), supplying a UIView that the bar button item is to display. The bar button item has no action and target; the UIView itself must somehow implement button behavior if that’s what you want. For example, the customView might be a UISegmentedControl, but then it is the UISegmentedControl’s target and action that give it button behavior.

Your custom view can and should use autolayout internally. Provide sufficient constraints to size the view from the inside out; otherwise, it may have no size (and will thus be invisible).

Bar button items in a toolbar are horizontally positioned automatically by the system. You can provide hints to help with this positioning. For example, you can supply an absolute width. Also, you can incorporate spacers into the toolbar; these are created with init(barButtonSystemItem:target:action:), but they have no visible appearance, and cannot be tapped. Place .flexibleSpace system items between the visible items to distribute the visible items equally across the width of the toolbar. There is also a .fixedSpace system item whose width lets you insert a space of defined size.

Navigation Items and Toolbar Items

What appears in a navigation bar (UINavigationBar) depends upon the navigation items (UINavigationItem) in its stack. In a navigation interface, the navigation controller will manage the navigation bar’s stack for you; your job is to configure each navigation item by setting properties of the navigationItem of each child view controller. The UINavigationItem properties are as follows (see also Chapter 12):

title
titleView

The title is a string. Setting a view controller’s title property sets the title of its navigationItem automatically, and is usually the best approach. The titleView can be any kind of UIView, and can implement further UIView functionality such as touchability.

In iOS 10 and before, the title or the titleView is displayed in the center of the navigation bar when this navigation item is at the top of the stack; if there is a titleView, it is shown instead of the title. Starting in iOS 11, the title can be shown at the bottom of the navigation bar, in which case both the title and the titleView can appear; I’ll explain more about that in a moment.

As with a custom view, the titleView should use autolayout internally, with sufficient constraints to size the view from the inside out.

prompt

An optional string to appear centered above everything else in the navigation bar. The navigation bar’s height will be increased to accommodate it.

rightBarButtonItem or rightBarButtonItems

A bar button item or, respectively, an array of bar button items to appear at the right side of the navigation bar; the first item in the array will be rightmost.

backBarButtonItem

When a view controller is pushed on top of this view controller, the navigation bar will display at its left a button pointing to the left, whose title is this view controller’s title. That button is this view controller’s navigation item’s backBarButtonItem. That’s right: the back button displayed in the navigation bar belongs, not to the top item (the navigationItem of the current view controller), but to the back item (the navigationItem of the view controller that is one level down in the stack).

Most of the time, the default behavior is the behavior you’ll want, and you’ll leave the back button alone. If you wish, though, you can customize the back button by setting a view controller’s navigationItem.backBarButtonItem so that it contains an image, or a title differing from the view controller’s title. The best technique is to provide a new UIBarButtonItem whose target and action are nil; the runtime will add a correct target and action, so as to create a working back button. Here’s how to create a back button with a custom image instead of a title:

let b = UIBarButtonItem(
    image:UIImage(named:"files"), style:.plain, target:nil, action:nil)
self.navigationItem.backBarButtonItem = b

A Bool property, hidesBackButton, allows the top navigation item to suppress display of the back item’s back bar button item. If you set this to true, you’ll probably want to provide some other means of letting the user navigate back.

The visible indication that the back button is a back button is a left-pointing chevron (the back indicator) that’s separate from the button itself. This chevron can also be customized, but it’s a feature of the navigation bar, not the bar button item: set the navigation bar’s backIndicatorImage and backIndicatorTransitionMask. (I’ll give an example in Chapter 12.) But if the back button is assigned a background image — not an internal image, as in the example I just gave, but a background image, by calling setBackButtonBackgroundImage — then the back indicator is removed; it is up to the background image to point left, if desired.

leftBarButtonItem or leftBarButtonItems

A bar button item or, respectively, an array of bar button items to appear at the left side of the navigation bar; the first item in the array will be leftmost. The leftItemsSupplementBackButton property, if set to true, allows both the back button and one or more left bar button items to appear.

Starting in iOS 11, a navigation bar can adopt an increased height in order to display the top item’s title in a large font below the bar button items. This is a navigation bar feature, its prefersLargeTitles property. In order to accomodate the possibility that different view controllers will have different preferences in this regard, a navigation item has a largeTitleDisplayMode, which may be one of the following:

.always

The navigation item’s title is displayed large if the navigation bar’s prefersLargeTitles is true.

.never

The navigation item’s title is not displayed large.

.automatic

The navigation item’s title display is the same as the title display of the back item — that is, of the navigation item preceding this one in the navigation bar’s stack. This is the default. The idea is that all navigation items pushed onto a navigation bar will display their titles in the same way, until a pushed navigation item declares .always or .never.

The navigation controller may grow or shrink its navigation bar to display or hide the large title as the contents of its view are scrolled — yet another reason why a nimble interface based on autolayout and the safe area is crucial.

A view controller’s navigation item can have its properties set at any time while being displayed in the navigation bar. This (and not direct manipulation of the navigation bar) is the way to change the navigation bar’s contents dynamically. For example, in one of my apps, we play music from the user’s library using interface in the navigation bar. The titleView is a progress view (UIProgressView, Chapter 12) that needs updating every second to reflect the playback position in the current song, and the right bar button should be either the system Play button or the system Pause button, depending on whether we are paused or playing. So I have a timer that periodically checks the state of the music player (self.mp); observe how we access the progress view and the right bar button by way of self.navigationItem:

// change the progress view
let prog = self.navigationItem.titleView!.subviews[0] as! UIProgressView
if let item = self.nowPlayingItem {
    let current = self.mp.currentPlaybackTime
    let total = item.playbackDuration
    prog.progress = Float(current / total)
} else {
    prog.progress = 0
}
// change the bar button
let whichButton : UIBarButtonItem.SystemItem? = {
    switch self.mp.currentPlaybackRate {
    case 0..<0.1:
        return .play
    case 0.1...1.0:
        return .pause
    default:
        return nil
    }
}()
if let which = whichButton {
    let bb = UIBarButtonItem(barButtonSystemItem: which,
        target: self, action: #selector(doPlayPause))
    self.navigationItem.rightBarButtonItem = bb
}

Each view controller to be pushed onto the navigation controller’s stack is responsible also for supplying the items to appear in the navigation interface’s toolbar, if there is one. To configure this, set the view controller’s toolbarItems property to an array of UIBarButtonItem instances. You can change the toolbar items even while the view controller’s view and current toolbarItems are showing, optionally with animation, by sending setToolbarItems(_:animated:) to the view controller.

Configuring a Navigation Controller

You configure a navigation controller by manipulating its stack of view controllers. This stack is the navigation controller’s viewControllers array property, though you will rarely need to manipulate that property directly.

The view controllers in a navigation controller’s viewControllers array are the navigation controller’s child view controllers; the navigation controller is the parent of the view controllers in the array. The navigation controller is also the navigationController of the view controllers in the array and of all their children; thus a child view controller at any depth can learn that it is contained by a navigation controller and can get a reference to that navigation controller. The navigation controller retains the array, and the array retains the child view controllers.

The normal way to manipulate a navigation controller’s stack is by pushing or popping one view controller at a time. When the navigation controller is instantiated, it is usually initialized with init(rootViewController:); this is a convenience method that assigns the navigation controller a single initial child view controller, the root view controller that goes at the bottom of the stack:

let fvc = FirstViewController()
let nav = UINavigationController(rootViewController:fvc)

Instead of init(rootViewController:), you might choose to create the navigation controller with init(navigationBarClass:toolbarClass:), in order to set a custom subclass of UINavigationBar or UIToolbar. Typically, this will be in order to customize the appearance of the navigation bar and toolbar; sometimes you’ll create, say, a UIToolbar subclass for no other reason than to mark this kind of toolbar as needing a certain appearance. I’ll explain about that in Chapter 12. If you use this initializer, you’ll have to set the navigation controller’s root view controller separately.

You can also set the UINavigationController’s delegate (adopting UINavigationControllerDelegate). The delegate receives messages before and after a child view controller’s view is shown.

If a navigation controller is the top-level view controller, it determines your app’s compensatory rotation behavior. To take a hand in that determination without having to subclass UINavigationController, make one of your objects the navigation controller’s delegate and implement these methods, as needed:

  • navigationControllerSupportedInterfaceOrientations(_:)

  • navigationControllerPreferredInterfaceOrientationForPresentation(_:)

A top-level navigation controller also determines your app’s status bar appearance. However, a navigation controller implements childForStatusBarHidden so that the actual decision is relegated to the child view controller whose view is currently being displayed: your prefersStatusBarHidden is consulted and obeyed.

Tip

But on a device without a bezel, such as the iPhone X, the status bar cannot be hidden if the navigation bar is present.

Your child view controllers can implement preferredStatusBarStyle, and the navigation controller’s childForStatusBarStyle defers to its top child view controller — but only if the navigation bar is hidden. If the navigation bar is showing, the navigation controller sets the status bar style based on the navigation bar’s barStyle — to .default if the bar style is .default, and to .lightContent if the bar style is .black. So the way for your view controller to set the status bar style, when the navigation bar is showing, is to set the navigation controller’s navigation bar style.

A navigation controller will typically appear on the screen initially containing just its root view controller, and displaying its root view controller’s view. There will be no back button, because there is no back item; there is nowhere to go back to. Subsequently, when the user asks to navigate to a new view, you (typically meaning code in the current view controller) obtain the next view controller (typically by creating it) and push it onto the stack by calling pushViewController(_:animated:) on the navigation controller. The navigation controller performs the animation, and displays the new view controller’s view:

let svc = SecondViewController(nibName: nil, bundle: nil)
self.navigationController!.pushViewController(svc, animated: true)

The command for going back is popViewController(animated:), but you might never need to call it yourself, as the runtime will call it for you when the user taps the back button to navigate back. When a view controller is popped from the stack, the viewControllers array removes and releases the view controller, which is usually permitted to go out of existence at that point.

Alternatively, there’s a second way to push a view controller onto the navigation controller’s stack, without referring to the navigation controller: show(_:sender:). This UIViewController method lets the caller be agnostic about the current interface situation: it pushes the view controller onto a navigation controller if the view controller to which it is sent is in a navigation interface, but presents it otherwise. I’ll talk more about this method in Chapter 9; meanwhile, I’ll continue using pushViewController(_:animated:) in my examples.

Instead of tapping the back button, the user can go back by dragging a pushed view controller’s view from the left edge of the screen. This is actually a way of calling popViewController(animated:), with the difference that the animation is interactive. (Interactive view controller transition animation is the subject of the next section.) The UINavigationController uses a UIScreenEdgePanGestureRecognizer to detect and track the user’s gesture. You can obtain a reference to this gesture recognizer as the navigation controller’s interactivePopGestureRecognizer; thus you can disable the gesture recognizer and prevent this way of going back, or you can mediate between your own gesture recognizers and this one (see Chapter 5).

You can manipulate the stack more directly if you wish. You can call popViewController(animated:) explicitly; to pop multiple items so as to leave a particular view controller at the top of the stack, call popToViewController(_:animated:), or to pop all the items down to the root view controller, call popToRootViewController(animated:). All of these methods return the popped view controller (or view controllers, as an array), in case you want to do something with them.

To set the entire stack at once, call setViewControllers(_:animated:). You can access the stack through the viewControllers property. Manipulating the stack directly is the only way, for instance, to remove or insert a view controller in the middle of the stack.

Tip

If a view controller needs a signal that it is being popped, override viewWillDisappear and see if self.isMovingFromParent is true.

The view controller at the top of the stack is the topViewController; the view controller whose view is displayed is the visibleViewController. Those will normally be the same, but they needn’t be, as the topViewController might present a view controller, in which case the presented view controller will be the visibleViewController. Other view controllers can be accessed through the viewControllers array by index number. The root view controller is at index 0; if the array’s count is c, the back view controller (the one whose navigationItem.backBarButtonItem is currently displayed in the navigation bar) is at index c-2.

The topViewController may need to communicate with the next view controller as the latter is pushed onto the stack, or with the back view controller as it itself is popped off the stack. The problem is parallel to that of communication between an original presenter and a presented view controller, which I discussed earlier in this chapter (“Communication with a Presented View Controller”), so I won’t say more about it here.

A child view controller will probably want to configure its navigationItem early in its lifetime, so as to be ready for display in the navigation bar by the time the view controller is handed as a child to the navigation controller. Apple warns (in the UIViewController class reference, under navigationItem) that loadView and viewDidLoad are not appropriate places to do this, because the circumstances under which the view is needed are not related to the circumstances under which the navigation item is needed. Apple’s own code examples routinely violate this warning, but it is probably best to override a view controller initializer for this purpose.

A navigation controller’s navigation bar is accessible as its navigationBar, and can be hidden and shown with setNavigationBarHidden(_:animated:). (It is possible, though not common, to maintain and manipulate a navigation stack through a navigation controller whose navigation bar never appears.) Its toolbar is accessible as its toolbar, and can be hidden and shown with setToolbarHidden(_:animated:).

A view controller also has the power to specify that its ancestor’s bottom bar (a navigation controller’s toolbar, or a tab bar controller’s tab bar) should be hidden as this view controller is pushed onto a navigation controller’s stack. To do so, set the view controller’s hidesBottomBarWhenPushed property to true. The trick is that you must do this very early, before the view loads; the view controller’s initializer is a good place. The bottom bar remains hidden from the time this view controller is pushed to the time it is popped, even if other view controllers are pushed and popped on top of it in the meantime.

A navigation controller can perform automatic hiding and showing of its navigation bar (and, if normally shown, its toolbar) in response to various situations, as configured by properties:

When tapped

If the navigation controller’s hidesBarsOnTap is true, a tap that falls through the top view controller’s view is taken as a signal to toggle bar visibility. The relevant gesture recognizer is the navigation controller’s barHideOnTapGestureRecognizer.

When swiped

If the navigation controller’s hidesBarsOnSwipe is true, an upward or downward swipe respectively hides or shows the bars. The relevant gesture recognizer is the navigation controller’s barHideOnSwipeGestureRecognizer.

In landscape

If the navigation controller’s hidesBarsWhenVerticallyCompact is true, bars are automatically hidden when the app rotates to landscape on the iPhone (and hidesBarsOnTap is treated as true, so the bars can be shown again by tapping).

When the user is typing

If the navigation controller’s hidesBarsWhenKeyboardAppears is true, bars are automatically hidden when the virtual keyboard appears (see Chapter 10).

You can configure a UINavigationController, or any view controller that is to serve in a navigation interface, in the nib editor. In the Attributes inspector, use a navigation controller’s Bar Visibility and Hide Bars checkboxes to determine the presence of the navigation bar and toolbar. The navigation bar and toolbar are themselves subviews of the navigation controller, and you can configure them with the Attributes inspector as well. A navigation bar has a Prefers Large Titles checkbox. A navigation controller’s root view controller can be specified; in a storyboard, there will be a “root view controller” relationship between the navigation controller and its root view controller. The root view controller is automatically instantiated together with the navigation controller.

A view controller in the nib editor has a Navigation Item where you can specify its title, its prompt, and the text of its back button. A navigation item has a Large Title pop-up menu, where you can set its largeTitleDisplayMode. You can drag Bar Button Items into a view controller’s navigation bar in the canvas to set the left buttons and right buttons of its navigationItem. Moreover, the Navigation Item has outlets, one of which permits you to set its titleView. Similarly, you can give a view controller Bar Button Items that will appear in the toolbar. (If a view controller in a nib doesn’t have a Navigation Item and you want to configure this view controller for use in a navigation interface, drag a Navigation Item from the Library onto the view controller.)

To start an iPhone project with a main storyboard that has a UINavigationController as its initial view controller, begin with the Master–Detail App template. Alternatively, start with the Single View App template, select the existing view controller, and choose Editor → Embed In → Navigation Controller (or choose Navigation Controller from the Embed button at the lower right of the canvas). A view controller to be subsequently pushed onto the navigation stack can be configured in the storyboard as the destination of a push segue; I’ll talk more about that later in this chapter.

Custom Transition

You can customize certain built-in transitions between view controller views:

Tab bar controller

When a tab bar controller changes which of its child view controllers is selected, by default there is no view animation; you can add a custom animation.

Navigation controller

When a navigation controller pushes or pops a child view controller, by default there is a sideways sliding view animation; you can replace this with a custom animation.

Presented view controller

When a view controller is presented or dismissed, there is a limited set of built-in view animations; you can supply a custom animation. Moreover, you can customize the ultimate size and position of the presented view, and how the presenting view is seen behind it; you can also provide ancillary views that remain during the presentation.

Given the extensive animation resources of iOS (see Chapter 4), this is an excellent chance for you to provide your app with variety and distinction. The view of a child view controller pushed onto a navigation controller’s stack needn’t arrive sliding from the side; it can expand by zooming from the middle of the screen, drop from above and fall into place with a bounce, snap into place like a spring, or whatever else you can dream up. A familiar example is Apple’s Calendar app, which transitions from a year to a month, in a navigation controller, by zooming in.

A custom transition animation can optionally be interactive: instead of tapping and causing an animation to take place, the user performs an extended gesture and gradually summons the new view to supersede the old one. The user can thus participate in the progress of the transition. A familiar example is the Photos app, which lets the user pinch a photo, in a navigation controller, to pop to the album containing it.

A custom transition animation can optionally be interruptible. You can provide a way for the user to pause the animation, possibly interact with the animated view by means of a gesture, and then resume (or cancel) the animation.

Noninteractive Custom Transition Animation

In the base case, you provide a custom animation that is not interactive. Configuring your custom animation requires three steps:

  1. Before the transition begins, you must have given the view controller in charge of the transition a delegate.

  2. As the transition begins, the delegate will be asked for an animation controller. You will supply a reference to some object adopting the UIViewControllerAnimatedTransitioning protocol (or nil to specify that the default animation, if any, should be used).

  3. The animation controller will be sent these messages:

    transitionDuration(using:)

    The animation controller must return the duration of the custom animation.

    animateTransition(using:)

    The animation controller should perform the animation.

    interruptibleAnimator(using:)

    Optional; if implemented, the animation controller should return an object adopting the UIViewImplicitlyAnimating protocol, which may be a property animator.

    animationEnded(_:)

    Optional; if implemented, the animation controller may perform cleanup following the animation.

I like to use a property animator to govern the animation; it will need to be accessible from multiple methods, so it must live in an instance property. I’ll type this instance property as an Optional wrapping a UIViewImplicitlyAnimating object:

var anim : UIViewImplicitlyAnimating?

I then implement all four animation controller methods:

transitionDuration(using:)

We’ll return a constant.

animateTransition(using:)

We’ll call interruptibleAnimator(using:) to obtain the property animator, and we’ll tell the property animator to start animating.

interruptibleAnimator(using:)

This is where all the real work happens. We’re being asked for the property animator. There is a danger that we will be called multiple times during the animation, so if the property animator already exists in our instance property, we simply return it; if it doesn’t exist, we create and configure it and assign it to our instance property, and then return it.

animationEnded(_:)

We’ll clean up any instance properties; at a minimum, we’ll set our property animator instance property to nil.

Now let’s get down to the nitty-gritty of what interruptibleAnimator(using:) actually does to configure the property animator and its animation. In general, a custom transition animation works as follows:

  1. The using: parameter is an object called the transition context (adopting the UIViewControllerContextTransitioning protocol). By querying the transition context, you can obtain:

    • The container view, an already existing view within which all the action is to take place.

    • The outgoing and incoming view controllers.

    • The outgoing and incoming views. These are probably the main views of the outgoing and incoming view controllers, but you should obtain the views directly from the transition context, just in case they aren’t. The outgoing view is already inside the container view.

    • The initial frame of the outgoing view, and the ultimate frame where the incoming view must end up.

  2. Having gathered this information, your mission is to put the incoming view into the container view and animate it in such a way as to end up at its correct ultimate frame. You may also animate the outgoing view if you wish.

  3. When the animation ends, your completion function must call the transition context’s completeTransition to tell it that the animation is over. In response, the outgoing view is removed automatically, and the animation comes to an end (and our animationEnded will be called).

As a simple example, I’ll use the transition between two child view controllers of a tab bar controller, when the user taps a different tab bar item. By default, this transition isn’t animated; one view just replaces the other. A possible custom animation is that the new view controller’s view should slide in from one side while the old view controller’s view should slide out the other side. The direction of the slide should depend on whether the index of the new view controller is greater or less than that of the old view controller. Let’s implement that.

Assume that the tab bar controller is our app’s root view controller. In that case, all the work can be done in the AppDelegate implementation. The tab bar controller is in charge of the transition, so the first step is to give it a delegate. I’ll do that in code, in the app delegate’s application(_:didFinishLaunchingWithOptions:), making the tab bar controller’s delegate be the app delegate itself:

(self.window!.rootViewController as! UITabBarController).delegate = self

The app delegate, in its role as UITabBarControllerDelegate, will be sent a message whenever the tab bar controller is about to change view controllers. That message is:

  • tabBarController(_:animationControllerForTransitionFrom:to:)

The second step is to implement that method. We must return an animation controller, namely, some object implementing UIViewControllerAnimatedTransitioning. I’ll return self:

extension AppDelegate : UITabBarControllerDelegate {
    func tabBarController(_ tabBarController: UITabBarController,
        animationControllerForTransitionFrom fromVC: UIViewController,
        to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
            return self
    }
}

(There is no particular reason why the animation controller should be self; I’m just using self to keep things simple. The animation controller can be any object — even a dedicated lightweight object instantiated just to govern this transition. There is also no particular reason why the animation controller should be the same object every time this method is called; depending on the circumstances, we could readily provide a different animation controller, or we could return nil to use the default transition — meaning, in this case, no animation.)

The third step is to implement the animation controller (UIViewControllerAnimatedTransitioning). I’ll start with stubs for the four methods we’re going to write:

extension AppDelegate : UIViewControllerAnimatedTransitioning {
    func transitionDuration(using ctx: UIViewControllerContextTransitioning?)
        -> TimeInterval {
            // ...
    }
    func animateTransition(using ctx: UIViewControllerContextTransitioning) {
        // ...
    }
    func interruptibleAnimator(using ctx: UIViewControllerContextTransitioning)
        -> UIViewImplicitlyAnimating {
            // ...
    }
    func animationEnded(_ transitionCompleted: Bool) {
        // ...
    }
}

Our transitionDuration must reveal in advance the duration of our animation:

func transitionDuration(using ctx: UIViewControllerContextTransitioning?)
    -> TimeInterval {
        return 0.4
}

(Again, the value returned needn’t be a constant; we could decide on the duration based on the circumstances. But the value returned here must be the same as the duration of the animation we’ll actually be constructing in interruptibleAnimator.)

Our animateTransition simply calls interruptibleAnimator to obtain the property animator, and tells it to animate:

func animateTransition(using ctx: UIViewControllerContextTransitioning) {
    let anim = self.interruptibleAnimator(using: ctx)
    anim.startAnimation()
}

The workhorse is interruptibleAnimator. If the property animator already exists, we unwrap it and return it, and that’s all:

func interruptibleAnimator(using ctx: UIViewControllerContextTransitioning)
    -> UIViewImplicitlyAnimating {
        if self.anim != nil {
            return self.anim!
        }
        // ...
}

If we haven’t returned, we need to form the property animator. First, we thoroughly query the transition context ctx to learn all about the parameters of this animation:

let vc1 = ctx.viewController(forKey:.from)!
let vc2 = ctx.viewController(forKey:.to)!
let con = ctx.containerView
let r1start = ctx.initialFrame(for:vc1)
let r2end = ctx.finalFrame(for:vc2)
let v1 = ctx.view(forKey:.from)!
let v2 = ctx.view(forKey:.to)!

Now we can prepare for our intended animation. In this case, we are sliding the views, so we need to decide the final frame of the outgoing view and the initial frame of the incoming view. We are sliding the views sideways, so those frames should be positioned sideways from the initial frame of the outgoing view and the final frame of the incoming view, which the transition context has just given us. Which side they go on depends upon the relative place of these view controllers among the children of the tab bar controller — is this to be a leftward slide or a rightward slide? Since the animation controller is the app delegate, we can get a reference to the tab bar controller the same way we did before:

let tbc = self.window!.rootViewController as! UITabBarController
let ix1 = tbc.viewControllers!.firstIndex(of:vc1)!
let ix2 = tbc.viewControllers!.firstIndex(of:vc2)!
let dir : CGFloat = ix1 < ix2 ? 1 : -1
var r1end = r1start
r1end.origin.x -= r1end.size.width * dir
var r2start = r2end
r2start.origin.x += r2start.size.width * dir

Now we’re ready to animate! We put the second view controller’s view into the container view at its initial frame, and animate our views:

v2.frame = r2start
con.addSubview(v2)
let anim = UIViewPropertyAnimator(duration: 0.4, curve: .linear) {
    v1.frame = r1end
    v2.frame = r2end
}

We must not neglect to supply the completion function that calls completeTransition:

anim.addCompletion { _ in
    ctx.completeTransition(true)
}

Our property animator is now formed! We retain it in our self.anim property, and we also return it:

self.anim = anim
return anim

That completes interruptibleAnimator. Finally, our animationEnded cleans up by destroying the property animator:

func animationEnded(_ transitionCompleted: Bool) {
    self.anim = nil
}

That’s all there is to it. Our example animation wasn’t very complex, but an animation needn’t be complex to be interesting, significant, and helpful to the user; I use this animation in my own apps, and I think it enlivens and clarifies the transition.

One possibility that I didn’t illustrate in my example is that you are free to introduce additional views temporarily into the container view during the course of the animation; you’ll probably want to remove them in the completion function. For example, you might make some interface object appear to migrate from one view controller’s view into the other (in reality you’d probably use a snapshot view; see Chapter 1).

Interactive Custom Transition Animation

With an interactive custom transition animation, the idea is that we track something the user is doing, typically by means of a gesture recognizer (see Chapter 5), and perform the “frames” of the transition in response.

To make a custom transition animation interactive, you supply, in addition to the animation controller, an interaction controller. This is an object adopting the UIViewControllerInteractiveTransitioning protocol. (This object needn’t be the same as the animation controller, but it often is, and in my examples it will be.) The runtime then calls the interaction controller’s startInteractiveTransition(_:) instead of the animation controller’s animateTransition(using:).

Configuring your custom animation requires the following steps:

  1. Before the transition begins, you must have given the view controller in charge of the transition a delegate.

  2. You’ll have a gesture recognizer that tracks the interactive gesture. When the gesture recognizer recognizes, it triggers the transition to the new view controller.

  3. As the transition begins, the delegate will be asked for an animation controller. You will return a UIViewControllerAnimatedTransitioning object.

  4. The delegate will also be asked for an interaction controller. You will return a UIViewControllerInteractiveTransitioning object (or nil to prevent the transition from being interactive). This object implements startInteractiveTransition(_:).

  5. The gesture recognizer continues by constantly calling updateInteractiveTransition(_:) on the transition context, as well as managing the frames of the animation.

  6. Sooner or later the gesture will end. At this point, we must decide whether to declare the transition completed or cancelled. A typical approach is to say that if the user performed more than half the full gesture, that constitutes completion; otherwise, it constitutes cancellation. We finish the animation accordingly.

  7. The animation is now completed, and its completion function is called. We must call the transition context’s finishInteractiveTransition or cancelInteractiveTransition, and then call its completeTransition(_:) with an argument stating whether the transition was finished or cancelled.

  8. Our animationEnded is called, and we clean up.

(You may be asking: why must we keep talking to our transition context throughout the process? The reason is that the animation might have a component separate from what you’re doing — for example, in the case of a navigation controller push or pop transition, the change in the appearance of the navigation bar. The transition context needs to coordinate that animation with the interactive gesture and with your animation. So you need to keep telling it where things are in the course of the interaction.)

As an example, I’ll describe how to make an interactive version of the tab bar controller transition animation that we developed in the previous section. The user will be able to drag from the edge of the screen to bring the tab bar controller’s adjacent view controller in from the right or from the left.

In the previous section, I cleverly planned ahead for this section. Almost all the code from the previous section can be left as is! I’ll build on that code, in such a way that the same custom transition animation can be either noninteractive (the user taps a tab bar item) or interactive (the user drags from one edge).

I’m going to need two more instance properties, in addition to self.anim:

var anim : UIViewImplicitlyAnimating?
var interacting = false
var context : UIViewControllerContextTransitioning?

The self.interacting property will be used as a signal that our transition is to be interactive. The self.context property is needed because the gesture recognizer’s action method is going to need access to the transition context. (Sharing the transition context through a property may seem ugly, but the elegant alternatives would make the example more complicated, so we’ll just do it this way.)

To track the user’s gesture, I’ll put a pair of UIScreenEdgePanGestureRecognizers into the interface. The gesture recognizers are attached to the tab bar controller’s view (tbc.view), as this will remain constant while the views of its view controllers are sliding across the screen. As in the previous section, all the code will go into the app delegate. In application(_:didFinishLaunchingWithOptions:), when I make the app delegate the tab bar controller’s delegate, I create the gesture recognizers and make the app delegate their delegate as well, so I can dictate which gesture recognizer is applicable to the current situation:

let tbc = self.window!.rootViewController as! UITabBarController
tbc.delegate = self
let sep = UIScreenEdgePanGestureRecognizer(target:self, action:#selector(pan))
sep.edges = UIRectEdge.right
tbc.view.addGestureRecognizer(sep)
sep.delegate = self
let sep2 = UIScreenEdgePanGestureRecognizer(target:self, action:#selector(pan))
sep2.edges = UIRectEdge.left
tbc.view.addGestureRecognizer(sep2)
sep2.delegate = self

Acting as the delegate of the two gesture recognizers, we prevent either pan gesture recognizer from operating unless there is another child of the tab bar controller available on that side of the current child:

extension AppDelegate : UIGestureRecognizerDelegate {
    func gestureRecognizerShouldBegin(_ g: UIGestureRecognizer) -> Bool {
        let tbc = self.window!.rootViewController as! UITabBarController
        var result = false
        if (g as! UIScreenEdgePanGestureRecognizer).edges == .right {
            result = (tbc.selectedIndex < tbc.viewControllers!.count - 1)
        }
        else {
            result = (tbc.selectedIndex > 0)
        }
        return result
    }
}

If the gesture recognizer action method pan is called, our interactive transition animation is to take place. I’ll break down the discussion according to the gesture recognizer’s states. In .began, I raise the self.interacting flag and trigger the transition by setting the tab bar controller’s selectedIndex:

@objc func pan(_ g:UIScreenEdgePanGestureRecognizer) {
    switch g.state {
    case .began:
        self.interacting = true
        let tbc = self.window!.rootViewController as! UITabBarController
        if g.edges == .right {
            tbc.selectedIndex = tbc.selectedIndex + 1
        } else {
            tbc.selectedIndex = tbc.selectedIndex - 1
        }
    // ...
    }
}

The transition begins. We are asked for our animation controller and our transition controller. We will supply a transition controller only if the self.interacting flag was raised; if the self.interacting flag is not raised, the user tapped a tab bar item and we are back in the preceding example:

extension AppDelegate: UITabBarControllerDelegate {
    func tabBarController(_ tabBarController: UITabBarController,
        animationControllerForTransitionFrom fromVC: UIViewController,
        to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
            return self
    }
    func tabBarController(_ tabBarController: UITabBarController,
        interactionControllerFor ac: UIViewControllerAnimatedTransitioning)
        -> UIViewControllerInteractiveTransitioning? {
            return self.interacting ? self : nil
    }
}

As a UIViewControllerInteractiveTransitioning adopter, our startInteractiveTransition(_:) is called instead of animateTransition(using:). However, our animateTransition(using:) is still in place, and still does the same job it did in the previous section. So we call it to obtain the property animator, and set the property animator instance property. But we do not tell the property animator to animate! We are interactive; that means we intend to manage the “frames” of the animation ourselves (see “Frozen View Animation”). We also set the UIViewControllerContextTransitioning property, so that the gesture recognizer’s action method can access it:

extension AppDelegate : UIViewControllerInteractiveTransitioning {
    func startInteractiveTransition(_ ctx:UIViewControllerContextTransitioning){
        self.anim = self.interruptibleAnimator(using: ctx)
        self.context = ctx
    }
}

The user’s gesture proceeds, and we are now back in the gesture recognizer’s action method, in the .changed state. We calculate the completed percentage of the gesture, and update both the property animator’s “frame” and the transition context:

case .changed:
    let v = g.view!
    let delta = g.translation(in:v)
    let percent = abs(delta.x/v.bounds.size.width)
    self.anim?.fractionComplete = percent
    self.context?.updateInteractiveTransition(percent)

Ultimately, the user’s gesture ends. Our goal now is to “hurry” to the start of the animation or the end of the animation, depending on how far the user got through the gesture. With a property animator, that’s really easy (see “Canceling a View Animation”):

case .ended:
    let anim = self.anim as! UIViewPropertyAnimator
    anim.pauseAnimation()
    if anim.fractionComplete < 0.5 {
        anim.isReversed = true
    }
    anim.continueAnimation(
        withTimingParameters:
        UICubicTimingParameters(animationCurve:.linear),
        durationFactor: 0.2)

The animation comes to an end, and the completion function that we gave our property animator in interruptibleAnimator is called. This is the one place in our interruptibleAnimator that needs to be a little different from the preceding example; we must send different messages to the transition context, depending on whether we finished at the end or reversed to the start:

anim.addCompletion { finish in
    if finish == .end {
        ctx.finishInteractiveTransition()
        ctx.completeTransition(true)
    } else {
        ctx.cancelInteractiveTransition()
        ctx.completeTransition(false)
    }
}

Finally, our animationEnded is called, and we clean up our instance properties:

func animationEnded(_ transitionCompleted: Bool) {
    self.interacting = false
    self.context = nil
    self.anim = nil
}

Another variation would be to make the custom transition animation interruptible. Again, this is straightforward thanks to the existence of property animators. While a view is in the middle of being animated, the property animator implements touchability of the animated view, and allows you to pause the animation. Thus, the user can be permitted (for example) to grab the animated view in the middle of the animation and move it around with the animation paused, and the animation can then resume when the user lets go of the view (as I demonstrated in “Hit-Testing During Animation”). You could equally incorporate these features into a custom transition animation.

You can use a UIPreviewInteraction (“3D Touch Press Gesture”) to drive a view controller custom transition animation through 3D touch. In that case, the user’s press is the gesture, and what advances the interactive custom transition animation is the UIPreviewInteraction and its delegate methods rather than a gesture recognizer and its action method.

Custom Presented View Controller Transition

With a presented view controller transition, you can customize not only the animation but also the final position of the presented view. Moreover, you can introduce ancillary views which remain in the scene while the presented view is presented, and are not removed until after dismissal is complete; for example, if the presented view is smaller than the presenting view and covers it only partially, you might add a dimming view between them, to darken the presenting view (just as a .formSheet presentation does).

There is no existing view to serve as the container view; therefore, when the presentation starts, the runtime constructs the container view and inserts it into the interface, leaving it there for only as long as the view remains presented. In the case of a .fullScreen presentation, the runtime also rips the presenting view out of the interface and inserts it into the container view, because you might want the presenting view to participate in the animation. For other styles of presentation, the container view is in front of the presenting view, which can’t be animated and is left in place as the presentation proceeds.

The work of customizing a presentation is distributed between two objects: the animation controller (or interaction controller) on the one hand, and a custom presentation controller on the other:

The animation controller

The animation controller should be responsible for only the animation, the movement of the presented view into its final position.

The custom presentation controller

The determination of the presented view’s final position is the job of the presentation controller. The presentation controller is also responsible for inserting any extra views, such as a dimming view, into the container view; Apple says that the animation controller animates the content, while the presentation controller animates the “chrome.”

This distribution of responsibilities may sound rather elaborate, but in fact the opposite is true: it greatly simplifies things, because if you don’t need one kind of customization you can simply omit it. If you supply an animation controller and no custom presentation controller, you dictate the animation, but the presented view will end up wherever the modal presentation style puts it. If you supply a custom presentation controller and no animation controller, a default transition style animation will be performed, but the presented view will end up at the position your custom presentation controller dictates.

Customizing the animation

I’ll start with a situation where we don’t use the presentation controller: all we want to do is customize the animation part of a built-in presentation style. The steps are almost completely parallel to how we customized a tab bar controller animation:

  1. Give the presented view controller a delegate. This means that we set the presented view controller’s transitioningDelegate property to an object adopting the UIViewControllerTransitioningDelegate protocol.

  2. The delegate will be asked for an animation controller, and will return an object adopting the UIViewControllerAnimatedTransitioning protocol. Unlike a tab bar controller or navigation controller, a presented view controller’s view undergoes two animations — the presentation and the dismissal — and therefore the delegate is asked separately for controllers:

    • animationController(forPresented:presenting:source:)

    • interactionControllerForPresentation(using:)

    • animationController(forDismissed:)

    • interactionControllerForDismissal(using:)

    You are free to customize just one animation, leaving the other at the default by not providing a controller for it.

  3. The animation controller will implement its four methods as usual — transitionDuration, animateTransition, interruptibleAnimator, and animationEnded.

To illustrate, let’s say we’re running on an iPad, and we want to present a view using the .formSheet presentation style. But instead of using any of the built-in animation types (transition styles), we’ll have the presented view appear to grow from the middle of the screen.

The only mildly tricky step is the first one. The problem is that the transitioningDelegate must be set very early in the presented view controller’s life — before the presentation begins. But the presented view controller doesn’t exist before the presentation begins. The most reliable approach, therefore, is for the presented view controller to assign its own delegate in its own initializer:

required init?(coder aDecoder: NSCoder) {
    super.init(coder:aDecoder)
    self.transitioningDelegate = self
}

The presentation begins, and we’re on to the second step. The transitioning delegate (UIViewControllerTransitioningDelegate) is asked for an animation controller; here, I’ll have it supply self once again, and I’ll do this only for the presentation, leaving the dismissal to use the default animation (and I’m not making this example interactive, so I don’t implement the interactionController methods):

func animationController(forPresented presented: UIViewController,
    presenting: UIViewController, source: UIViewController)
    -> UIViewControllerAnimatedTransitioning? {
        return self
}

The third step is that the animation controller (UIViewControllerAnimatedTransitioning) is called upon to implement the animation. Our implementations of transitionDuration, animateTransition, and animationEnded are the usual boilerplate, so I’ll show only interruptibleAnimator, which configures the property animator; observe that we don’t care about the .from view controller, which remains in place during the presentation (indeed, its view isn’t even in the container view):

func interruptibleAnimator(using ctx: UIViewControllerContextTransitioning)
    -> UIViewImplicitlyAnimating {
        if self.anim != nil {
            return self.anim!
        }
        let vc2 = ctx.viewController(forKey:.to)
        let con = ctx.containerView
        let r2end = ctx.finalFrame(for:vc2!)
        let v2 = ctx.view(forKey:.to)!
        v2.frame = r2end
        v2.transform = CGAffineTransform(scaleX: 0.1, y: 0.1)
        v2.alpha = 0
        con.addSubview(v2)
        let anim = UIViewPropertyAnimator(duration: 0.4, curve: .linear) {
            v2.alpha = 1
            v2.transform = .identity
        }
        anim.addCompletion { _ in
            ctx.completeTransition(true)
        }
        self.anim = anim
        return anim
}

If we wish to customize both animation and dismissal using the same animation controller, there is a complication: the roles of the view controllers are reversed in the mind of the transition context. On presentation, the presented view controller is the .to view controller, but on dismissal, it is the .from view controller. For a presentation that isn’t .fullScreen, the unused view is nil, so you can distinguish the cases by structuring your code like this:

let v1 = ctx.view(forKey:.from)
let v2 = ctx.view(forKey:.to)
if let v2 = v2 { // presenting
    // ...
} else if let v1 = v1 { // dismissing
    // ...
}

Customizing the presentation

Now let’s involve the presentation controller: we will customize the final frame of the presented view controller’s view, and we’ll even add some “chrome” to the presentation. This will require some additional steps:

  1. In addition to setting a transitioningDelegate, we must set the presented view controller’s modalPresentationStyle to .custom.

  2. The result of the preceding step is that the delegate (our adopter of UIViewControllerTransitioningDelegate) is sent an additional message:

    • presentationController(forPresented:presenting:source:)

    (The source: parameter is what I have termed the “original presenter.”) Your mission is to return an instance of a custom UIPresentationController subclass. This will then be the presented view controller’s presentation controller during the course of this presentation, from the time presentation begins to the time dismissal ends. You must create this instance by calling (directly or indirectly) the designated initializer:

    • init(presentedViewController:presenting:)

  3. By means of appropriate overrides in your UIPresentationController subclass, you participate in the presentation, dictating the presented view’s final position (frameOfPresentedViewInContainerView) and adding “chrome” to the presentation as desired.

The UIPresentationController has properties pointing to the presentingViewController as well the presentedViewController and the presentedView, plus the presentationStyle set by the presented view controller. It also obtains the containerView, which it subsequently communicates to the animation controller’s transition context. It has some methods and properties that you can override in your subclass; you only need to override the ones that require customization for your particular implementation:

frameOfPresentedViewInContainerView

The final position of the presented view. If there is an animation controller, it will receive this from the transition context’s finalFrame(for:) method.

presentationTransitionWillBegin
presentationTransitionDidEnd
dismissalTransitionWillBegin
dismissalTransitionDidEnd

Use these events as signals to add or remove “chrome” (extra views) to the container view.

containerViewWillLayoutSubviews
containerViewDidLayoutSubviews

Use these layout events as signals to update the “chrome” views if needed.

shouldRemovePresentersView

The default is false, except that of course it is true for a standard .fullScreen presentation, meaning that the presenting view is ripped out of the interface at the end of the presentation transition. You can return true for a custom presentation, but it would be rare to do this; even if the presented view completely covers the presenting view, there is no harm in leaving the presenting view in place.

A presentation controller is not a view controller, but UIPresentationController adopts some protocols that UIViewController adopts, and thus gets the same resizing-related messages that a UIViewController gets, as I described earlier in this chapter. It adopts UITraitEnvironment, meaning that it has a traitCollection and participates in the trait collection inheritance hierarchy, and receives the traitCollectionDidChange(_:) message. It also adopts UIContentContainer, meaning that it receives willTransition(to:with:) and viewWillTransition(to:with:).

To illustrate the use of a custom presentation controller, I’ll expand the preceding example to implement a custom presentation style that looks like a .formSheet even on an iPhone. The first step is to set the presentation style to .custom at the same time that we set the transitioning delegate:

required init?(coder aDecoder: NSCoder) {
    super.init(coder:aDecoder)
    self.transitioningDelegate = self
    self.modalPresentationStyle = .custom // *
}

The result (step two) is that this extra UIViewControllerTransitioningDelegate method is called so that we can provide a custom presentation controller:

func presentationController(forPresented presented: UIViewController,
    presenting: UIViewController?, source: UIViewController)
    -> UIPresentationController? {
        let pc = MyPresentationController(
            presentedViewController: presented, presenting: presenting)
        return pc
}

Everything else happens in our implementation of our UIPresentationController subclass (named MyPresentationController). To make the presentation look like a .formSheet, we inset the presented view’s frame:

override var frameOfPresentedViewInContainerView : CGRect {
    return super.frameOfPresentedViewInContainerView.insetBy(dx:40, dy:40)
}

We could actually stop at this point! The presented view now appears in the correct position. However, the presenting view is appearing undimmed behind it. Let’s add dimming, by inserting a translucent dimming view into the container view. Note that we are careful to deal with the possibility of subsequent rotation:

override func presentationTransitionWillBegin() {
    let con = self.containerView!
    let shadow = UIView(frame:con.bounds)
    shadow.backgroundColor = UIColor(white:0, alpha:0.4)
    con.insertSubview(shadow, at: 0)
    shadow.autoresizingMask = [.flexibleWidth, .flexibleHeight]
}

Again, this works perfectly, but now I don’t like what happens when the presented view is dismissed: the dimming view vanishes suddenly at the end of the dismissal. I’d rather have the dimming view fade out, and I’d like it to fade out in coordination with the dismissal animation. The way to arrange that is through the object vended by the presented view controller’s transitionCoordinator property (a UIViewControllerTransitionCoordinator); in particular, we can call its animate(alongsideTransition:completion:) method to add our own animation:

override func dismissalTransitionWillBegin() {
    let con = self.containerView!
    let shadow = con.subviews[0]
    let tc = self.presentedViewController.transitionCoordinator!
    tc.animate(alongsideTransition: { _ in
        shadow.alpha = 0
    })
}

Once again, we could stop at this point. But I’d like to add a further refinement. A .formSheet view has rounded corners. I’d like to make our presented view look the same way:

override var presentedView : UIView? {
    let v = super.presentedView!
    v.layer.cornerRadius = 6
    v.layer.masksToBounds = true
    return v
}

Finally, for completeness, it would be nice, during presentation, to dim the appearance of any button titles and other tinted interface elements visible through the dimming view, to emphasize that they are disabled:

override func presentationTransitionDidEnd(_ completed: Bool) {
    let vc = self.presentingViewController
    let v = vc.view
    v?.tintAdjustmentMode = .dimmed
}
override func dismissalTransitionDidEnd(_ completed: Bool) {
    let vc = self.presentingViewController
    let v = vc.view
    v?.tintAdjustmentMode = .automatic
}

Transition Coordinator

In the previous section, I mentioned that a view controller has a transitionCoordinator, which is typed as a UIViewControllerTransitionCoordinator. We have also encountered this sort of object as the second parameter of viewWillTransition(to:with:) and willTransition(to:with:). It’s time to say a bit more about what a transition coordinator is.

A transition coordinator exists only during a transition between view controllers, such as presentation or pushing. Its actual class is of no importance; UIViewControllerTransitionCoordinator is a protocol. This protocol, in turn, conforms to the UIViewControllerTransitionCoordinatorContext protocol, just like a transition context; indeed, it is a kind of wrapper around the transition context.

A view controller can therefore use its transitionCoordinator to find out about the transition it is currently involved in. Moreover, as I’ve already said, it can take advantage of animate(alongsideTransition:completion:) to add animation of its view’s internal interface as part of a transition animation. This works equally for a custom animation or a built-in animation; in fact, the point is that the view controller can behave agnostically with regard to how its own view is being animated.

In this example, a presented view controller animates part of its interface into place as the animation proceeds (whatever that animation may be):

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    if let tc = self.transitionCoordinator {
        tc.animate(alongsideTransition:{ _ in
            self.buttonTopConstraint.constant += 200
            self.view.layoutIfNeeded()
        })
    }
}

The transition coordinator implements an additional method that might be of occasional interest:

notifyWhenInteractionChanges(_:)

The argument you pass is a function to be called; the transition context is the function’s parameter. Your function is called whenever the transition changes between being interactive and being noninteractive; this might be because the interactive transition was cancelled.

In this example, a navigation controller has pushed a view controller, and now the user is popping it interactively (using the default drag-from-the-left-edge gesture). If the user cancels, the back view controller can hear about it, like this:

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    let tc = self.transitionCoordinator
    tc?.notifyWhenInteractionChanges { ctx in
        if ctx.isCancelled {
            // ...
        }
    }
}
Warning

I have not found any occasion when the child of a tab bar controller has a non-nil transition coordinator — even though you may have given the tab bar controller’s transition a custom animation. I regard this as a bug.

Page View Controller

A page view controller (UIPageViewController) is like a book that can be viewed only one page at a time. The user, by a gesture, can navigate in one direction or the other to see the next or the previous page, successively — like turning the pages of a book.

Actually, a page view controller only seems to have multiple pages. In effect, it has only the one page that the user is looking at. That page is the view of its one child view controller. The page view controller navigates to another page by releasing its existing child view controller and replacing it with another. This is a very efficient architecture: it makes no difference whether the page view controller lets the user page through three pages or ten thousand pages, because each page is created in real time, on demand, and exists only as long as the user is looking at it.

The page view controller’s children are its viewControllers. In general, there will be at most one of them (though there is a rarely used configuration in which a page view controller can have two pages at a time, as I’ll explain in a moment). The page view controller is its current child’s parent.

Preparing a Page View Controller

To create a UIPageViewController in code, use its designated initializer:

  • init(transitionStyle:navigationOrientation:options:)

Here’s what the parameters mean:

transitionStyle:

The animation type during navigation (UIPageViewController.TransitionStyle). Your choices are:

  • .pageCurl

  • .scroll (sliding)

navigationOrientation:

The direction of navigation (UIPageViewController.NavigationOrientation). Your choices are:

  • .horizontal

  • .vertical

options:

A dictionary. Possible keys are (UIPageViewController.OptionsKey):

.spineLocation

If you’re using the .pageCurl transition style, this is the position of the pivot line around which those page curl transitions rotate. The value (UIPageViewController.SpineLocation) is one of the following:

  • .min (left or top)

  • .mid (middle; in this configuration there are two children, and two pages are shown at once)

  • .max (right or bottom)

.interPageSpacing

If you’re using the .scroll transition style, this is the spacing between successive pages, visible as a gap during the transition (the default is 0).

You configure the page view controller’s initial content by handing it its initial child view controller(s). You do that by calling this method:

  • setViewControllers(_:direction:animated:completion:)

Here’s what the parameters mean:

viewControllers:

An array of one view controller — unless you’re using the .pageCurl transition style and the .mid spine location, in which case it’s an array of two view controllers.

direction:

The animation direction (UIPageViewController.NavigationDirection). This probably won’t matter when you’re assigning the page view controller its initial content, as you are not likely to want any animation. Possible values are:

  • .forward

  • .backward

animated:, completion:

A Bool and a completion function.

To allow the user to page through the page view controller, you also assign the page view controller a dataSource, which should conform to the UIPageViewControllerDataSource protocol. The dataSource is told whenever the user starts to change pages, and should respond by immediately providing another view controller whose view will constitute the new page. Typically, the data source will create this view controller on the spot.

Here’s a minimal example. Each page in the page view controller is to portray an image of a named Pep Boy. The first question is where the pages will come from. My data model consists of an array (self.pep) of the string names of the three Pep Boys:

let pep : [String] = ["Manny", "Moe", "Jack"]

To match these, I have three eponymous image files (manny, moe, and jack), portraying each Pep Boy. I’ve also got a UIViewController subclass called Pep, capable of displaying a Pep Boy’s image in an image view. I initialize a Pep object with Pep’s designated initializer init(pepBoy:), supplying the name of a Pep Boy from the array; the Pep object sets its own boy property:

init(pepBoy boy:String) {
    self.boy = boy
    super.init(nibName: nil, bundle: nil)
}

Pep’s viewDidLoad then fetches the corresponding image and assigns it as the image of a UIImageView within its own view:

override func viewDidLoad() {
    super.viewDidLoad()
    self.pic.image = UIImage(named:self.boy.lowercased())
}

At any given moment, then, our page view controller will have one Pep instance as its child, and thus will portray a Pep Boy. Here’s how I create the page view controller itself (in my app delegate):

// make a page view controller
let pvc = UIPageViewController(
    transitionStyle: .scroll, navigationOrientation: .horizontal)
// give it an initial page
let page = Pep(pepBoy: self.pep[0])
pvc.setViewControllers([page], direction: .forward, animated: false)
// give it a data source
pvc.dataSource = self
// put its view into the interface
self.window!.rootViewController = pvc

That’s sufficient to show the first page, but I haven’t yet explained how to allow the user to navigate to a new page! That’s the job of the data source, as I’ll describe in the next section.

The page view controller is a UIViewController, and its view must get into the interface by standard means. You can make the page view controller the window’s rootViewController, as I do here; you can make it a presented view controller; you can make it a child view controller of a tab bar controller or a navigation controller. If you want the page view controller’s view to be a subview of a custom view controller’s view, that view controller must be a custom container view controller, as I’ll describe later in this chapter.

Page View Controller Navigation

We now have a page view controller’s view in our interface, itself containing and displaying the view of one Pep view controller that is its child. In theory, we have three pages, because we have three Pep Boys and their images — but the page view controller knows about only one of them. Just as with a navigation controller, you don’t supply (or even create) another page until the moment comes to navigate to it. When that happens, one of these data source methods will be called:

  • pageViewController(_:viewControllerAfter:)

  • pageViewController(_:viewControllerBefore:)

The job of those methods is to return the requested successive view controller — or nil, to signify that there is no further page in this direction. Your strategy for doing that will depend on how your model maintains the data. My data, as you’ll recall, is an array of unique strings:

let pep : [String] = ["Manny", "Moe", "Jack"]

And a Pep view controller has one of those strings as its boy property. So all I have to do is start with the current Pep view controller’s boy and find the previous name or the next name in the array:

func pageViewController(_ pvc: UIPageViewController,
    viewControllerAfter vc: UIViewController) -> UIViewController? {
        let boy = (vc as! Pep).boy
        let ix = self.pep.firstIndex(of:boy)! + 1
        if ix >= self.pep.count {
            return nil
        }
        return Pep(pepBoy: self.pep[ix])
}
func pageViewController(_ pvc: UIPageViewController,
    viewControllerBefore vc: UIViewController) -> UIViewController? {
        let boy = (vc as! Pep).boy
        let ix = self.pep.firstIndex(of:boy)! - 1
        if ix < 0 {
            return nil
        }
        return Pep(pepBoy: self.pep[ix])
}

We now have a working page view controller! The user, with a sliding gesture, can page through it, one page at a time. When the user reaches the first page or the last page, it is impossible to go further in that direction.

Tip

A .scroll style page view controller may cache its view controllers in advance. Thus, you should make no assumptions about when these data source methods will be called. If you need to be notified when the user is actually turning the page, use the delegate (which I’ll describe later), not the data source.

You can also, at any time, call setViewControllers to change programmatically what page is being displayed, possibly with animation. In this way, you can “jump” to a page other than a successive page (something that the user cannot do with a gesture).

Page indicator

If you’re using the .scroll transition style, the page view controller can optionally display a page indicator (a UIPageControl, see Chapter 12). The user can look at this to get a sense of what page we’re on, and can tap to the left or right of it to navigate. To get the page indicator, you must implement two more data source methods; they are consulted in response to setViewControllers. We called that method initially to configure the page view controller; if we never call it again (because the user simply keeps navigating to the next or previous page), these data source methods won’t be called again either, because they don’t need to be: the page view controller will keep track of the current index on its own. Here’s my implementation for the Pep Boy example:

func presentationCount(for pvc: UIPageViewController) -> Int {
    return self.pep.count
}
func presentationIndex(for pvc: UIPageViewController) -> Int {
    let page = pvc.viewControllers![0] as! Pep
    let boy = page.boy
    return self.pep.firstIndex(of:boy)!
}

Unfortunately, the page view controller’s page indicator by default has white dots and a clear background, so it is invisible in front of a white background. You’ll want to customize it to change that. There is no direct access to it, so it’s simplest to use the appearance proxy (Chapter 12). For example:

let proxy = UIPageControl.appearance()
proxy.pageIndicatorTintColor = UIColor.red.withAlphaComponent(0.6)
proxy.currentPageIndicatorTintColor = .red
proxy.backgroundColor = .yellow

Navigation gestures

If you’ve assigned the page view controller the .pageCurl transition style, the user can navigate by tapping at either edge of the view or by dragging across the view. These gestures are detected through two gesture recognizers, which you can access through the page view controller’s gestureRecognizers property. The documentation suggests that you might change where the user can tap or drag by attaching them to a different view, and other customizations are possible as well. In this code, I change the behavior of a .pageCurl page view controller (pvc) so that the user must double tap to request navigation:

for g in pvc.gestureRecognizers {
    if let g = g as? UITapGestureRecognizer {
        g.numberOfTapsRequired = 2
    }
}

Of course you are also free to add to the user’s stock of gestures for requesting navigation. You can supply any controls or gesture recognizers that make sense for your app, and respond by calling setViewControllers. For example, if you’re using the .scroll transition style, there’s no tap gesture recognizer, so the user can’t tap at either edge of the page view controller’s view to request navigation. Let’s change that. I’ve added invisible views at either edge of my Pep view controller’s view, with tap gesture recognizers attached. When the user taps, the tap gesture recognizer fires, and the action method posts a notification whose object is the tap gesture recognizer:

@IBAction func tap (_ sender: UIGestureRecognizer?) {
    NotificationCenter.default.post(name:Pep.tap, object: sender)
}

In the app delegate, I have registered to receive this notification. When it arrives, I use the tap gesture recognizer’s view’s tag to learn which view was tapped; I then navigate accordingly (pvc is the page view controller):

NotificationCenter.default.addObserver(
    forName:Pep.tap, object: nil, queue: .main) { n in
        let g = n.object as! UIGestureRecognizer
        let which = g.view!.tag
        let vc0 = pvc.viewControllers![0]
        guard let vc = (which == 0 ?
            self.pageViewController(pvc, viewControllerBefore: vc0) :
            self.pageViewController(pvc, viewControllerAfter: vc0))
            else {return}
        let dir : UIPageViewController.NavigationDirection =
            which == 0 ? .reverse : .forward
        UIApplication.shared.beginIgnoringInteractionEvents()
        pvc.setViewControllers([vc], direction: dir, animated: true) {
            _ in
            UIApplication.shared.endIgnoringInteractionEvents()
        }
    }
}

In that code, I turn off user interaction when the page animation starts and turn it back on when the animation ends. The reason is that otherwise we can crash (or get into an incoherent state) if the user taps during the animation.

Other Page View Controller Configurations

It is possible to assign a page view controller a delegate (UIPageViewControllerDelegate), which gets an event when the user starts turning the page and when the user finishes turning the page, and can change the spine location dynamically in response to a change in device orientation. As with a tab bar controller’s delegate or a navigation controller’s delegate, a page view controller’s delegate also gets messages allowing it to specify the page view controller’s app rotation policy, so there’s no need to subclass UIPageViewController solely for that purpose.

One further bit of configuration applicable to a .pageCurl page view controller is the isDoubleSided property. If it is true, the next page occupies the back of the previous page. The default is false, unless the spine is in the middle, in which case it’s true and can’t be changed. Your only option here, therefore, is to set it to true when the spine isn’t in the middle, and in that case the back of each page would be a sort of throwaway page, glimpsed by the user during the page curl animation.

A page view controller in a storyboard lets you configure its transition style, navigation orientation, page spacing, spine location, and isDoubleSided property. (It also has delegate and data source outlets, but you’re not allowed to connect them to other view controllers, because you can’t draw an outlet from one scene to another in a storyboard.) It has no child view controller relationship, so you can’t set the page view controller’s initial child view controller in the storyboard; you’ll have to complete the page view controller’s initial configuration in code.

Container View Controllers

UITabBarController, UINavigationController, and UIPageViewController are all built-in parent view controllers: you hand them a child view controller and they do all the work, retaining that child view controller and putting its view into the interface inside their own view. What if you wanted your own view controller to do the same sort of thing?

Your UIViewController subclass can act as a custom parent view controller, managing child view controllers and putting their views into the interface. A custom parent view controller of this sort is called a container view controller. Your own view controller, behaving as a container view controller, becomes like one of the built-in parent view controllers, except that you get to define what it does — what it means for a view controller to be a child of this kind of parent view controller, how many children it has, which of its children’s views appear in the interface and where they appear, and so on. A container view controller can also participate actively in the business of trait collection inheritance and view resizing.

An example appears in Figure 6-3 — and the construction of that interface is charted in Figure 6-4. We have a page view controller, but it is not the root view controller, and its view does not occupy the entire interface. How is that achieved? It is not achieved by simply grabbing the page view controller’s view and plopping it into the interface. You must never do that. My RootViewController is a container view controller, and the UIPageViewController is its child.

To put a view controller’s view into the interface ourselves, there must be a container view controller, and it must follow certain rules. The other view controller must be made its child, and the parent is then permitted — as long it follows the rules — to put the child view controller’s view into the interface, as a subview of its own view.

Adding and Removing Children

A view controller has a children array; that’s what gives it the power to be a parent. You must not, however, just wantonly populate this array. A child view controller needs to receive certain definite events at particular moments:

  • As it becomes a child view controller

  • As its view is added to and removed from the interface

  • As it ceases to be a child view controller

Therefore, to act as a parent view controller, your UIViewController subclass must fulfill certain responsibilities:

Adding a child

When a view controller is to become your view controller’s child, your view controller must do these things, in this order:

  1. Send addChild(_:) to itself, with the child as argument. The child is automatically added to your children array and is retained.

  2. Get the child view controller’s view into the interface (as a subview of your view controller’s view), if that’s what adding a child view controller means.

  3. Send didMove(toParent:) to the child view controller, with your view controller as argument.

Removing a child

When a view controller is to cease being your view controller’s child, your view controller must do these things, in this order:

  1. Send willMove(toParent:) to the child, with a nil argument.

  2. Remove the child view controller’s view from your interface.

  3. Send removeFromParent to the child. The child is automatically removed from your children array and is released.

This is a clumsy and rather confusing dance. The underlying reason for it is that a child view controller must always receive willMove(toParent:) followed by didMove(toParent:) (and your own child view controllers can take advantage of these events however you like). But it turns out that you don’t always send both these messages explicitly, because:

  • addChild(_:) sends willMove(toParent:) for you automatically.

  • removeFromParent sends didMove(toParent:) for you automatically.

Thus, in each case you must send manually the other message, the one that adding or removing a child view controller doesn’t send for you — and of course you must send it so that everything happens in the correct order, as dictated by the rules I just listed.

When you do this dance correctly, the proper parent–child relationship results: the container view controller can refer to its children as its children, and any child has a reference to the parent as its parent. If you don’t do it correctly, all sorts of bad things can happen; in a worst-case scenario, the child view controller won’t even survive, and its view won’t work correctly, because the view controller was never properly retained as part of the view controller hierarchy (see “View Controller Hierarchy”). So do the dance correctly!

The initial child view controller

Example 6-1 provides a schematic approach for how to obtain an initial child view controller and put its view into the interface. (Alternatively, a storyboard can do this work for you, with no code, as I’ll explain later in this chapter.)

Example 6-1. Adding an initial child view controller
let vc = // whatever; this is the initial child view controller
self.addChild(vc) // "will" called for us
// insert view into interface between "will" and "did"
self.view.addSubview(vc.view)
vc.view.frame = // whatever, or use constraints
// when we call add, we must call "did" afterward
vc.didMove(toParent: self)

In many cases, that’s all you’ll need. You have a parent view controller and a child view controller, and they are paired permanently, for the lifetime of the parent. That’s how Figure 6-3 behaves: RootViewController has a page view controller as its child, and the page view controller’s view as its own view’s subview, for the entire lifetime of the app.

To illustrate, I’ll use the same page view controller that I used in my earlier examples, the one that displays Pep Boys; but this time, its view won’t occupy the entire interface. My root view controller will be called RootViewController. I’ll create and configure my page view controller as a child of RootViewController, in RootViewController’s viewDidLoad; note how carefully and correctly I perform the dance:

let pep : [String] = ["Manny", "Moe", "Jack"]
override func viewDidLoad() {
    super.viewDidLoad()
    let pvc = UIPageViewController(
        transitionStyle: .scroll, navigationOrientation: .horizontal)
    pvc.dataSource = self
    self.addChild(pvc) // step 1
    self.view.addSubview(pvc.view) // step 2
    // ... configure frame or constraints here ...
    pvc.didMove(toParent: self) // step 3
    let page = Pep(pepBoy: self.pep[0])
    pvc.setViewControllers([page], direction: .forward, animated: false)
}

Replacing a child view controller

It is also possible to replace one child view controller’s view in the interface with another (comparable to how UITabBarController behaves when a different tab bar item is selected). The simplest, most convenient way to do that is with this parent view controller instance method:

  • transition(from:to:duration:options:animations:completion:)

That method manages the stages in good order, adding the view of one child view controller (to:) to the interface before the transition and removing the view of the other child view controller (from:) from the interface after the transition, and seeing to it that the child view controllers receive lifetime events (such as viewWillAppear(_:)) at the right moment. Here’s what the last three arguments are for:

options:

A bitmask (UIView.AnimationOptions) comprising the same possible options that apply to any view transition (see “Transitions”).

animations:

An animations function. This may be used for animating views other than the two views being managed by the transition animation specified in the options: argument; alternatively, if none of the built-in transition animations is suitable, you can animate the transitioning views yourself here (they are both in the interface during this function).

completion:

A completion function. It will be important if the transition involves the removal or addition of a child view controller. At the time when you call transition..., both view controllers must be children of the parent view controller; so if you’re going to remove one of the view controllers as a child, you’ll do it in the completion function. Similarly, if you owe a new child view controller a didMove(toParent:) call, you’ll use the completion function to fulfill that debt.

Here’s an example. To keep things simple, suppose that our view controller has just one child view controller at a time, and displays the view of that child view controller within its own view. So let’s say that when our view controller is handed a new child view controller, it substitutes that new child view controller for the old child view controller and replaces the old child view controller’s view with the new child view controller’s view. Here’s code that does that correctly; the view controllers are fromvc and tovc:

// we have already been handed the new view controller
// set up the new view controller's view's frame
tovc.view.frame = // ... whatever
// must have both as children before we can transition between them
self.addChild(tovc) // "will" called for us
// when we call remove, we must call "will" (with nil) beforehand
fromvc.willMove(toParent: nil)
// then perform the transition
self.transition(
    from:fromvc, to:tovc,
    duration:0.4, options:.transitionFlipFromLeft,
    animations:nil) { _ in
        // when we call add, we must call "did" afterward
        tovc.didMove(toParent: self)
        fromvc.removeFromParent() // "did" called for us
}

If we’re using constraints to position the new child view controller’s view, where will we set up those constraints? Before you call transition... is too soon, as the new child view controller’s view is not yet in the interface. The completion function is too late: if the view is added with no constraints, it will have no initial size or position, so the animation will be performed and then the view will suddenly seem to pop into existence as we provide its constraints. The animations function turns out to be a very good place:

// must have both as children before we can transition between them
self.addChild(tovc) // "will" called for us
// when we call remove, we must call "will" (with nil) beforehand
fromvc.willMove(toParent: nil)
// then perform the transition
self.transition(
    from:fromvc, to:tovc,
    duration:0.4, options:.transitionFlipFromLeft,
    animations: {
        tovc.view.translatesAutoresizingMaskIntoConstraints = false
        // ... configure tovc.view constraints here ...
    }) { _ in
        // when we call add, we must call "did" afterward
        tovc.didMove(toParent: self)
        fromvc.removeFromParent() // "did" called for us
}

If the built-in transition animations are unsuitable, you can omit the options: argument and provide your own animation in the animations function, at which time both views are in the interface. In this example, I animate a substitute view (an image view showing a snapshot of tovc.view) to grow from the top left corner; then I configure the real view’s constraints and remove the substitute:

// tovc.view.frame is already set
let r = UIGraphicsImageRenderer(size:tovc.view.bounds.size)
let im = r.image { ctx in
    tovc.view.layer.render(in:ctx.cgContext)
}
let iv = UIImageView(image:im)
iv.frame = .zero
self.view.addSubview(iv)
tovc.view.alpha = 0 // hide the real view
// must have both as children before we can transition between them
self.addChild(tovc) // "will" called for us
// when we call remove, we must call "will" (with nil) beforehand
fromvc.willMove(toParent: nil)
// then perform the transition
self.transition(
    from:fromvc, to:tovc,
    duration:0.4, // no options:
    animations: {
        iv.frame = tovc.view.frame // animate bounds change
        // ... configure tovc.view constraints here ...
    }) { _ in
        tovc.view.alpha = 1
        iv.removeFromSuperview()
        // when we call add, we must call "did" afterward
        tovc.didMove(toParent: self)
        fromvc.removeFromParent() // "did" called for us
}

Status Bar, Traits, and Resizing

A parent view controller, instead of dictating the status bar appearance through its own implementation of preferredStatusBarStyle or prefersStatusBarHidden, can defer the responsibility to one of its children, by overriding these properties:

  • childForStatusBarStyle

  • childForStatusBarHidden

That is, as I’ve already mentioned, what a UITabBarController does. Your custom parent view controller can do the same thing.

A container view controller also participates in trait collection inheritance. In fact, you might insert a container view controller into your view controller hierarchy just to take advantage of this feature. A parent view controller has the amazing ability to lie to a child view controller about the environment, thanks to this method:

  • setOverrideTraitCollection(_:forChild:)

The first parameter is a UITraitCollection that will be combined with the inherited trait collection and communicated to the specified child.

Why would you want to lie to a child view controller about its environment? Well, imagine that we’re writing an iPad app, and we have a view controller whose view can appear either fullscreen or as a small subview of a parent view controller’s main view. The view’s interface might need to be different when it appears in the smaller size. You could configure that difference using size classes (conditional constraints) in the nib editor, with one interface for a .regular horizontal size class (iPad) and another interface for a .compact horizontal size class (iPhone). Then, when the view is to appear in its smaller size, we lie to its view controller and tell it that this is an iPhone:

let vc = // the view controller we're going to use as a child
self.addChild(vc) // "will" called for us
let tc = UITraitCollection(horizontalSizeClass: .compact)
self.setOverrideTraitCollection(tc, forChild: vc) // heh heh
vc.view.frame = // whatever
self.view.addSubview(vc.view)
vc.didMove(toParent: self)

UIPresentationController has a similar power, through its overrideTraitCollection property, allowing it to lie to its presented view controller about the inherited trait collection. That is why, for example, a .formSheet presented view controller has a .compact horizontal size class even on an iPad.

A parent view controller sets the size of a child view controller’s view. A child view controller, however, can express a preference as to what size it would like its view to be, by setting its own preferredContentSize property. The chief purpose of this property is to be consulted by a parent view controller when this view controller is its child. This property is a preference and no more; no law says that the parent must consult the child, or that the parent must obey the child’s preference.

If a view controller’s preferredContentSize is set while it is a child view controller, the runtime automatically communicates this fact to the parent view controller by calling this UIContentContainer method:

  • preferredContentSizeDidChange(forChildContentContainer:)

The parent view controller may implement this method to consult the child’s preferredContentSize, and may change the child’s view’s size in response if it so chooses.

A parent view controller, as an adopter of the UIContentContainer protocol, is also responsible for communicating to its children that their sizes are changing and what their new sizes will be. It is the parent view controller’s duty to implement this method:

size(forChildContentContainer:withParentContainerSize:)

Should be implemented to return each child view controller’s correct size at any moment. Failure to implement this method will cause the child view controller to be handed the wrong size in its implementation of viewWillTransition(to:with:) — it will be given the parent’s new size rather than its own new size!

If your parent view controller implements viewWillTransition(to:with:), it should call super so that viewWillTransition(to:with:) will be passed down to its children. This works even if your implementation is explicitly changing the size of a child view controller, provided you have implemented size(forChildContentContainer:withParentContainerSize:) to return the new size.

Peek and Pop

Suppose the current view controller can perform a transition to a new view controller (such as pushing or presenting the new view controller). And suppose the device has 3D touch. Then you can permit the user to do a partial press to preview the new view controller’s view from within the current view controller’s view, without actually performing the transition. The user can then either back off the press completely, in which case the preview vanishes, or do a full press, in which case the transition is performed. Apple calls this peek and pop.

Apple’s own apps use peek and pop extensively. For example, in the Mail app, viewing a mailbox’s list of messages, the user can peek at a message’s content; in the Calendar app, viewing a month, the user can peek at a day’s events; and so on.

The preview during peek and pop is only a preview; the user can’t interact with it. In effect, the preview is just a snapshot. However, to give the preview itself some additional functionality, it can be accompanied by menu items, similar to an action sheet (see Chapter 13). The user slides the preview upward to reveal the menu items. The user can then tap a menu item to perform its action, or tap the preview to back out and return to the original view controller.

Wrapping a peek and pop implementation around your view controller transition involves three stages: registration, peeking, and popping.

Registration

To implement peek and pop at all, your source view controller (the one that the user would transition from if the full transition were performed) must first register by calling this method:

registerForPreviewing(with:sourceView:)

The first parameter is an object adopting the UIViewControllerPreviewingDelegate protocol (typically self). The second parameter is a touchable view within which you want the user to be able to press in order to summon a preview. You can call this method multiple times to register multiple source views.

This method also returns a value, a system-supplied context manager conforming to the UIViewControllerPreviewing protocol. However, for straightforward peek and pop you won’t need to capture this object; it will be supplied again in the delegate method calls.

Peeking

Let’s say the user now uses 3D touch to press somewhere on the screen. In order for your UIViewControllerPreviewingDelegate adopter to be called, this press must be within a registered touchable view; if it is within a subview of the registered view, the subview must itself be touchable (because otherwise hit-testing would fail to report the press in the first place). This means it’s time to peek. The first delegate method is called:

previewingContext(_:viewControllerForLocation:)

The first parameter is the context manager I mentioned a moment ago. The second parameter, the location:, is the point where the user is pressing, in sourceView coordinates; you can examine this to decide whether the press is within an area corresponding to an element for which you want to trigger peek and pop. To prevent peeking, return nil. Otherwise:

  • Optionally, set the context manager’s sourceRect to the region, expressed in source view coordinates, that will stay sharp while the rest of the interface blurs to indicate that peeking is about to take place. If you don’t do this, the source view itself will be used.

  • Instantiate an appropriate destination view controller and return it. The runtime will snapshot the view controller’s view and present that snapshot as the preview.

Popping

Now let’s say the user, while previewing, continues to press harder and reaches full force. This means it’s time to pop. The second delegate method is called:

previewingContext(_:commit:)

The first parameter is the context manager; the second parameter is the view controller you provided in the previous delegate method. Your job is now to perform the actual transition.

In all likelihood, you will transition to the view controller that arrives as the second parameter. Still, no law requires this; you might implement peek by displaying a subview or simplified interface, using another view controller, and then pop to the real view controller.

Here’s a simple example. First, I’ll describe my transition. I have a view controller with three buttons: Manny, Moe, and Jack. My view controller is a container view controller; when the user taps a button, I create the corresponding Pep view controller (whose view contains that Pep boy’s image) and make it my view controller’s child, displaying its view in my view controller’s view:

@IBAction func doShowBoy(_ sender : UIButton) {
    let title = sender.title(for: .normal)!
    let pep = Pep(pepBoy: title)
    self.transitionContainerTo(pep)
}
func transitionContainerTo(_ pep:Pep) {
    let oldvc = self.children[0]
    pep.view.frame = self.container.bounds
    self.addChild(pep)
    oldvc.willMove(toParent: nil)
    self.transition(
        from: oldvc, to: pep,
        duration: 0.2, options: .transitionCrossDissolve,
        animations: nil) { _ in
            pep.didMove(toParent: self)
            oldvc.removeFromParent()
    }
}

Now I want to wrap peek and pop around that transition by allowing the user to apply 3D touch on those three buttons. The buttons are subviews of a common superview, self.buttonSuperview. I’ll register that superview for previewing in my container view controller’s viewDidLoad:

override func viewDidLoad() {
    super.viewDidLoad()
    self.registerForPreviewing(with: self, sourceView: self.buttonSuperview)
}

In the first delegate method, I hit-test the press location; if the user is pressing on a button, I set the context manager’s sourceRect, instantiate the corresponding Pep view controller, and return it:

func previewingContext(_ ctx: UIViewControllerPreviewing,
    viewControllerForLocation loc: CGPoint) -> UIViewController? {
        let sv = ctx.sourceView
        guard let button =
            sv.hitTest(loc, with: nil) as? UIButton else {return nil}
        let title = button.title(for: .normal)!
        let pep = Pep(pepBoy: title)
        ctx.sourceRect = button.convert(button.bounds, to:sv)
        return pep
}

In the second delegate method, I perform the transition, exactly as if the user had tapped a button:

func previewingContext(_ ctx: UIViewControllerPreviewing,
    commit vc: UIViewController) {
        if let pep = vc as? Pep {
            self.transitionContainerTo(pep)
        }
}

Creating menu items to accompany the preview is the job of the destination view controller. All it has to do is override the previewActionItems property to supply an array of UIPreviewActionItems. A UIPreviewActionItem can be a UIPreviewAction, which is basically a simple tappable menu item. Alternatively, it can be a UIPreviewActionGroup, consisting of an array of UIPreviewActions; this looks like a menu item, but when the user taps it the menu items vanish and are replaced by the group’s menu items, giving in effect a second level of menu hierarchy. A UIPreviewActionItem can have a style: .default, .selected (the menu item has a checkmark), or .destructive (the menu item has a warning red color).

I’ll extend the preceding example to demonstrate the use of a UIPreviewActionGroup and the .selected style. My Pep view controller overrides previewActionItems. The user can tap a Colorize menu item to see a secondary menu of three possible colors; tapping one of those will presumably somehow colorize this Pep Boy. The user can also tap a Favorite menu item to make this Pep Boy the favorite (implemented through UserDefaults); if this Pep Boy is already the favorite, this menu item has a checkmark:

override var previewActionItems: [UIPreviewActionItem] {
    // example of submenu (group)
    let col1 = UIPreviewAction(title:"Blue", style: .default) {
        action, vc in // ...
    }
    let col2 = UIPreviewAction(title:"Green", style: .default) {
        action, vc in // ...
    }
    let col3 = UIPreviewAction(title:"Red", style: .default) {
        action, vc in // ...
    }
    let group = UIPreviewActionGroup(
        title: "Colorize", style: .default, actions: [col1, col2, col3])
    // example of selected style
    let favKey = "favoritePepBoy"
    let style : UIPreviewAction.Style =
        self.boy == UserDefaults.standard.string(forKey:favKey) ?
        .selected : .default
    let fav = UIPreviewAction(title: "Favorite", style: style) {
        action, vc in
        if let pep = vc as? Pep {
            UserDefaults.standard.set(pep.boy, forKey:favKey)
        }
    }
    return [group, fav]
}

The function passed to the UIPreviewAction initializer receives as parameters the UIPreviewAction and the view controller instance (so that you can refer to the view controller without causing a retain cycle). I take advantage of this in the Favorite menu item implementation, pulling out the boy instance property string to use as the value saved into user defaults, thus identifying which Pep Boy is now the favorite.

You can configure peek and pop in a storyboard, without code. In the nib editor, select a triggered segue emanating from a tappable interface object (an action segue) and check the Peek & Pop checkbox in the Attributes inspector. If you need to add code, similar to the delegate methods, use the pop-up menus to provide custom segues (I’ll explain later what custom segues are).

Storyboards

A storyboard is a way of creating view controllers automatically. In particular, it is a way of performing automatically the kind of view controller management I’ve described throughout this chapter, such as creating a view controller and making it the child of another, or creating a view controller and transitioning to it.

A storyboard doesn’t necessarily reduce the amount of code you’ll have to write, but it does clarify the relationships between your view controllers over the course of your app’s lifetime. Instead of having to hunt around in each of your classes to see which class creates which view controller and when, you can view and manage the chain of view controller creation graphically in the nib editor.

A storyboard is a collection of view controller nibs, which are displayed as its scenes. Each view controller is instantiated from its own nib, as needed, and will then obtain its view, as needed — typically from a view nib that you’ve configured in the same scene by editing the view controller’s view. I described this process in “How Storyboards Work”. As I explained there, a view controller can be instantiated from a storyboard in various ways, which we can now discuss in more detail:

Manual instantiation

Your code can instantiate a view controller manually from a storyboard, by calling one of these methods:

  • instantiateInitialViewController

  • instantiateViewController(withIdentifier:)

Initial view controller

If your app has a main storyboard, as specified by its Info.plist, that storyboard’s initial view controller will be instantiated and assigned as the window’s rootViewController automatically as the app launches. To specify that a view controller is a storyboard’s initial view controller, check the “Is Initial View Controller” checkbox in its Attributes inspector. This will cause any existing initial view controller to lose its initial view controller status. The initial view controller is distinguished graphically in the canvas by an arrow pointing to it from the left, and in the document outline by the presence of the Storyboard Entry Point.

Relationship

Two built-in parent view controllers can specify their children directly in the storyboard, setting their viewControllers array:

  • UITabBarController can specify multiple children (its “view controllers”).

  • UINavigationController can specify its single initial child (its “root view controller”).

To add a view controller as a viewControllers child to one of those parent view controller types, Control-drag from the parent view controller to the child view controller; in the little HUD that appears, choose the appropriate listing under Relationship Segue. The result is a relationship whose source is the parent and whose destination is the child. The destination view controller will be instantiated automatically when the source view controller is instantiated, and will be assigned into its viewControllers array, thus making it a child and retaining it.

Triggered segue

A triggered segue configures a future situation, when the segue will be triggered. At that time, one view controller that already exists will cause the instantiation of another, bringing the latter into existence automatically. Two types of triggered segue are particularly common (their names in the nib editor depend on whether the “Use Trait Variations” checkbox is checked in the File inspector):

Show (formerly Push)

The future view controller will be pushed onto the stack of the navigation controller of which the existing view controller is already a child.

The name Show comes from the show(_:sender:) method, which pushes a view controller onto the parent navigation controller if there is one, but behaves adaptively if there is not (I’ll talk more about that in Chapter 9). A Show segue from a view controller that is not a navigation controller’s child will present the future view controller rather than pushing it, as there is no navigation stack to push onto. Setting up a Show segue without a navigation controller and then wondering why there is no push is a common beginner mistake.

Present Modally (formerly Modal)

The future view controller will be a presented view controller (and the existing view controller will be its original presenter).

Unlike a relationship, a triggered segue does not have to emanate from a view controller (a manual segue). It can emanate from certain kinds of gesture recognizer, or from a tappable view, such as a button or a table view cell, in the first view controller’s view; this is a graphical shorthand signifying that the segue should be triggered, bringing the second view controller into existence, when a tap or other gesture occurs (an action segue).

To create a triggered segue, Control-drag from the tappable object in the first view controller, or from the first view controller itself, to the second view controller. In the little HUD that appears, choose the type of segue you want. If you dragged from the view controller, this will be a manual segue; if you dragged from a tappable object, it will be an action segue.

pios 1912b
Figure 6-9. The storyboard of an app

Figure 6-9 shows the storyboard of a small test app. The initial view controller (at the left) is a tab bar controller. It will be instantiated automatically when the app launches. Through two “view controllers” relationships, it has two children, which will both be instantiated automatically together with the tab bar controller:

  • The tab bar controller’s first child (upper row) is a navigation controller, which itself, through a “root view controller” relationship, has one child, which will also be instantiated automatically and will be placed at the start of the navigation controller’s stack when the navigation controller is instantiated. That child has a triggered Show segue to another view controller, giving it the ability in the future to create that view controller and push it onto the navigation controller’s stack.

  • The tab bar controller’s second child (lower row) has a triggered Present Modally segue to another view controller, giving it the ability in the future to create that view controller and present it.

Triggered Segues

A triggered segue is a true segue (as opposed to relationships, which are not really segues at all). The most common types are Show (Push) and Present Modally (Modal). A segue is a full-fledged object, an instance of UIStoryboardSegue, and it can be configured in the nib editor through its Attributes inspector. However, it is not instantiated by the loading of a nib, and it cannot be pointed to by an outlet. Rather, it will be instantiated when the segue is triggered, at which time its designated initializer will be called, namely init(identifier:source:destination:).

A segue’s source and destination are the two view controllers between which it runs. The segue is directional, so the source and destination are clearly distinguished. The source view controller is the one that will exist already, before the segue is triggered; the destination view controller will be instantiated when the segue is triggered, along with the segue itself.

A segue’s identifier is a string. You can set this string for a segue in a storyboard through its Attributes inspector; that’s useful when you want to trigger the segue manually in code (you’ll specify it by means of its identifier), or when you have code that can receive a segue as parameter and you need to distinguish which segue this is.

Triggered segue behavior

The default behavior of a segue, when it is triggered, is exactly the behavior of the corresponding manual transition described earlier in this chapter:

Show (Push)

The segue is going to call pushViewController(_:animated:) (if we are in a navigation interface). To set animated: to false, uncheck the Animates checkbox in the Attributes inspector.

Present Modally (Modal)

The segue is going to call present(_:animated:completion:). To set animated: to false, uncheck the Animates checkbox in the Attributes inspector. Other presentation options, such as the modal presentation style and the modal transition style, can be set in the destination view controller’s Attributes inspector or in the segue’s Attributes inspector (the segue settings will override the destination view controller settings).

You can further customize a triggered segue’s behavior by providing your own UIStoryboardSegue subclass. The key thing is that you must implement your custom segue’s perform method, which will be called after the segue is triggered and instantiated, in order to do the actual transition from one view controller to another. You can do this even for a push segue or a modal segue: in the Attributes inspector for the segue, you specify your UIStoryboardSegue subclass, and in that subclass, you call super in your perform implementation.

Let’s say, for example, that you want to add a custom transition animation to a modal segue. You can do this by writing a segue class that makes itself the destination view controller’s transitioning delegate in its perform implementation before calling super:

class MyCoolSegue: UIStoryboardSegue {
    override func perform() {
        let dest = self.destination
        dest.modalPresentationStyle = .custom
        dest.transitioningDelegate = self
        super.perform()
    }
}
extension MyCoolSegue: UIViewControllerTransitioningDelegate {
    func animationController(forPresented presented: UIViewController,
        presenting: UIViewController,
        source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
            return self
    }
    // ...
}
extension MyCoolSegue: UIViewControllerAnimatedTransitioning {
    func transitionDuration(using ctx: UIViewControllerContextTransitioning?)
        -> TimeInterval {
            return 0.8
    }
    // ...
}

The rest is then exactly as in “Custom Presented View Controller Transition”. MyCoolSegue is the UIViewControllerTransitioningDelegate, so its animationController(forPresented:...) will be called. MyCoolSegue is also the UIViewControllerAnimatedTransitioning object, so its transitionDuration and so forth will be called. In short, we are now off to the races with a custom presented view controller transition, with all the code living inside MyCoolSegue — a pleasant encapsulation of functionality.

You can also create a completely custom segue. To do so, in the HUD when you Control-drag to create the segue, ask for a Custom segue, and then, in the Attributes inspector, specify your UIStoryboardSegue subclass. Again, you must override perform, but now you don’t call super — the whole transition is up to you! Your perform implementation can access the segue’s identifier, source, and destination properties. The destination view controller has already been instantiated, but that’s all; it is entirely up to your code make this view controller a child view controller or presented view controller and cause its view to appear in the interface.

How a segue is triggered

A triggered segue will be triggered in one of two ways:

Through a user gesture

If a segue emanates from a gesture recognizer or from a tappable view, it becomes an action segue, meaning that it will be triggered automatically when the tap or other gesture occurs.

Your source view controller class can prevent an action segue from being triggered. To do so, override this method:

shouldPerformSegue(withIdentifier:sender:)

Sent when an action segue is about to be triggered. Returns a Bool (and the default is true), so if you don’t want this segue triggered on this occasion, return false.

In code

If a segue emanates from a view controller as a whole, it is a manual segue, and triggering it is up to your code. Send this message to the source view controller:

performSegue(withIdentifier:sender:)

Triggers a segue whose source is this view controller. The segue will need an identifier in the storyboard so that you can specify it here! shouldPerformSegue(withIdentifier:sender:) will not be called, because if you didn’t want the segue triggered, you wouldn’t have called performSegue in the first place.

An action segue with an identifier can be treated as a manual segue: that is, you can trigger it by calling performSegue, thus doing in code what the user could have done by tapping.

View controller communication

When a segue is triggered, the destination view controller is instantiated automatically; your code does not instantiate it. This raises a crucial question: how are you going to communicate between the source view controller and the destination view controller? This, you’ll remember, was the subject of an earlier section of this chapter (“Communication with a Presented View Controller”), where I used this code as an example:

let svc = SecondViewController(nibName: nil, bundle: nil)
svc.data = "This is very important data!"
svc.delegate = self
self.present(svc, animated:true)

In that code, the first view controller creates the second view controller, and therefore has a reference to it at that moment. Thus, it has an opportunity to communicate with it, passing along some data to it, and setting itself as its delegate, before presenting it. With a modal segue, however, the second view controller is instantiated for you, and the segue itself is going to call present(_:animated:completion:). So when and how will the first view controller be able to set svc.data and set itself as svc’s delegate?

The answer is that, after a segue has instantiated the destination view controller but before the segue is actually performed, the source view controller is sent prepare(for:sender:). The first parameter is the segue, and the segue has a reference to the destination view controller — so this is the moment when the source view controller and the destination view controller meet! The source view controller can thus perform configurations on the destination view controller, hand it data, and so forth. The source view controller can work out which segue is being triggered by examining the segue’s identifier and destination properties, and the sender is the interface object that was tapped to trigger the segue (or, if performSegue(withIdentifier:sender:) was called in code, whatever object was supplied as the sender: argument).

So, for example:

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.identifier == "second" {
        let svc = segue.destination as! SecondViewController
        svc.data = "This is very important data!"
        svc.delegate = self
    }
}

This solves the communication problem. Unfortunately, it solves it in a clumsy way; prepare(for:sender:) feels like a blunt instrument. The destination arrives typed as a generic UIViewController, and it is up to your code to know its actual type and cast it before configuring it. If more than one segue emanates from a view controller, they are all bottlenecked through the same prepare(for:sender:) implementation, which devolves into an ugly collection of conditions to distinguish them. I regard this aspect of the storyboard architecture as flawed.

Container Views and Embed Segues

The only parent view controllers for which you can create relationship segues specifying their children in a storyboard are the built-in UITabBarController and UINavigationController. That’s because the nib editor understands how they work. If you write your own custom container view controller (“Container View Controllers”), the nib editor doesn’t even know that your view controller is a container view controller, so it can’t be the source of a relationship segue.

Nevertheless, you can perform some initial parent–child configuration of your custom container view controller in a storyboard, if your situation conforms to these assumptions:

  • Your parent view controller will have one initial child view controller.

  • You want the child view controller’s view placed somewhere in the parent view controller’s view.

To configure your parent view controller in a storyboard, drag a Container View object from the Library into the parent view controller’s view in the canvas. The result is a view, together with an embed segue leading from it to an additional child view controller. You can then specify the child view controller’s correct class in its Identity inspector. Alternatively, delete the child view controller, Control-drag from the container view to some other view controller, and specify an Embed segue in the HUD.

When an embed segue is triggered, the destination view controller is instantiated and made the source view controller’s child, and its view is placed precisely inside the container view as its subview. Thus, the container view is not only a way of generating the embed segue, but also a way of specifying where you want the child view controller’s view to go. The entire child-addition dance is performed correctly and automatically for you: addChild(_:) is called, the child’s view is put into the interface, and didMove(toParent:) is called.

An embed segue is a triggered segue. It can have an identifier, and the standard messages are sent to the source view controller when the segue is triggered. At the same time, it has this similarity to a relationship: when the source (parent) view controller is instantiated, the runtime wants to trigger the segue automatically, instantiating the child view controller and embedding its view in the container view now. If that isn’t what you want, override shouldPerformSegue(withIdentifier:sender:) in the parent view controller to return false for this segue, and call performSegue(withIdentifier:sender:) later when you want the child view controller instantiated.

The parent view controller is sent prepare(for:sender:) before the child’s view loads. At this time, the child has not yet been added to the parent’s children array. If you allow the segue to be triggered when the parent view controller is instantiated, then by the time the parent’s viewDidLoad is called, the child’s viewDidLoad has already been called, the child has already been added to the parent’s children, and the child’s view is already inside the parent’s view.

If you subsequently want to replace the child view controller’s view with another child view controller’s view in the interface, you will do so in code, probably by calling transition(from:to:duration:options:animations:completion:) as I described earlier in this chapter. If you really want to, you can configure this through a storyboard by using a custom segue.

Storyboard References

When you create a segue in the storyboard (a triggered segue or a relationship), you don’t have to Control-drag to a view controller as the destination; instead, you can Control-drag to a storyboard reference which you have previously added to the canvas of this storyboard. A storyboard reference is a placeholder for a specific view controller. Thus, instead of a large and complicated network of segues running all over your storyboard, possibly crisscrossing in confusing ways, you can effectively jump through the storyboard reference to the actual destination view controller.

To specify what view controller a storyboard reference stands for, you need to perform two steps:

  1. Select the view controller and, in the Identity inspector, give it a Storyboard ID.

  2. Select the storyboard reference and, in the Attributes inspector, enter that same Storyboard ID as its Referenced ID.

But wait — there’s more! The referenced view controller doesn’t even have to be in the same storyboard as the storyboard reference. You can use a storyboard reference to jump to a view controller in a different storyboard. With a storyboard reference that leads into a different storyboard, that storyboard is loaded automatically when needed. This allows you to organize your app’s interface into multiple storyboards.

To configure a storyboard reference to refer to a view controller in a different storyboard, use the Storyboard pop-up menu in its Attributes inspector. The rule is that if you specify the Storyboard but not the Referenced ID, the storyboard reference stands for the target storyboard’s initial view controller (the one marked as the Storyboard Entry Point in that storyboard’s document outline). If you do specify the Referenced ID, then of course the storyboard reference stands for the view controller with that Storyboard ID in the target storyboard. (I find, as a practical matter, that things work best if you always specify both the storyboard reference’s Storyboard and its Referenced ID.)

Unwind Segues

Here’s an interesting puzzle: Storyboards and segues would appear to be useful only half the time, because segues are asymmetrical. There is a push segue but no pop segue. There is a present modally segue but no dismiss segue.

The reason, in a nutshell, is that a triggered segue cannot “go back.” A triggered segue instantiates the destination view controller; it creates a new view controller instance. But when dismissing a presented view controller or popping a pushed view controller, we don’t need any new view controller instances. We want to return, somehow, to an existing instance of a view controller.

Beginners sometimes make a triggered segue from view controller A to view controller B, and then try to express the notion “go back” by making another triggered segue from view controller B to view controller A. The result is a vicious cycle of segues, with presentation piled on presentation, or push piled on push, one view controller instantiated on top of another on top of another. Don’t do that. (Unfortunately, the nib editor doesn’t alert you to this mistake.)

The solution is an unwind segue. An unwind segue does let you express the notion “go back” in a storyboard. Basically, it lets you jump to any view controller that is already instantiated further up your view controller hierarchy, destroying the source view controller and any intervening view controllers in good order.

Creating an unwind segue

Before you can create an unwind segue, you must implement an unwind method in the class of some view controller represented in the storyboard. This should be a method marked @IBAction as a hint to the storyboard editor, and taking a single parameter, a UIStoryboardSegue. You can call it unwind if you like, but the name doesn’t really matter:

@IBAction func unwind(_ seg: UIStoryboardSegue) {
    // ...
}

Think of this method as a marker, specifying that the view controller in which it appears can be the destination for an unwind segue. It is, in fact, a little more than a marker: it will also be called when the unwind segue is triggered. But its marker functionality is much more important — so much so that, in many cases, you won’t give this method any code at all. Its presence, and its name, are what matters.

Now you can create an unwind segue. Doing so involves the use of the Exit proxy object that appears in every scene of a storyboard. This is what the Exit proxy is for! Control-drag from the view controller you want to go back from, or from something like a button in that view controller’s view, connecting it to the Exit proxy object in the same scene (Figure 6-10). A little HUD appears, listing all the known unwind methods (similar to how action methods are listed in the HUD when you connect a button to its target). Click the name of the unwind method you want. You have now made an unwind segue, bound to that unwind method.

pios 1912c
Figure 6-10. Creating an unwind segue

How an unwind segue works

When the unwind segue is triggered, the following steps are performed:

  1. If this is an action segue, the source view controller’s shouldPerformSegue(withIdentifier:sender:) is called — just as for a normal segue. This is your chance to stop the whole process dead at this point by returning false.

  2. The name of the unwind method to which the unwind segue is bound is only a name. The unwind segue’s actual destination view controller is unknown! Therefore, the runtime now starts walking up the view controller hierarchy looking for a destination view controller. Put simply, the first view controller it finds that implements the unwind method will, by default, be the destination view controller.

Assume now that the destination view controller has been found. Then the runtime proceeds to perform the segue, as follows:

  1. The source view controller’s prepare(for:sender:) is called with the segue as the first parameter — just as for a normal segue. The two view controllers are now in contact (because the other view controller is the segue’s destination). This is an opportunity for the source view controller to hand information to the destination view controller before being destroyed! (Thus, an unwind segue is an alternative to delegation as a way of putting one view controller into communication with another: see “Communication with a Presented View Controller”.)

  2. The destination view controller’s unwind method is called. Its parameter is the segue. The two view controllers are now in contact again (because the other view controller is the segue’s source). It is perfectly reasonable, as I’ve already said, for the unwind method body to be empty; the unwind method’s real purpose is to mark this view controller as the destination view controller.

  3. The segue is actually performed, destroying the source view controller and any intervening view controllers up to (but not including) the destination view controller, in good order.

Now I’ll go back and explain in detail how the destination view controller is found, and how the segue is actually performed. This is partly out of sheer interest — they are both devilishly clever — and partly in case you need to customize the process. You can skip the discussion if the technical details aren’t of interest to you.

How the destination view controller is found

The process of locating the destination view controller starts by walking up the view controller hierarchy. What do I mean by “up” the hierarchy? Well, every view controller has either a parent or a presentingViewController, so the next view controller up the hierarchy is that view controller. However, it might also be necessary to walk back down the hierarchy, to a child (at some depth) of one of the parents we encounter. Here’s how the walk proceeds:

  1. At each step up the view controller hierarchy, the runtime sends this view controller the following event:

    • allowedChildrenForUnwinding(from:)

    This view controller’s job is to supply an array of its own direct children. The array can be empty, but it must be an array. To help form this array, the view controller calls this method:

    • childContaining(_:)

    This tells the view controller which of its own children is, or is the ultimate parent of, the source view controller. We don’t want to go down that branch of the view hierarchy; that’s the branch we just came up. So this view controller subtracts that view controller from the array of its own child view controllers, and returns the resulting array.

  2. There are two possible kinds of result from the previous step (the value returned from allowedChildren...):

    There are children

    If the previous step yielded an array with one or more child view controllers in it, the runtime performs step 1 on all of them (stopping if it finds the destination), thus going down the view hierarchy.

    There are no children

    If, on the other hand, the previous step yielded an empty array, the runtime asks this same view controller the following question:

    • canPerformUnwindSegueAction(_:from:withSender:)

    The default implementation of this method is simply to call responds(to:) on self, asking whether this view controller contains an implementation of the unwind method we’re looking for. The result is a Bool. If it is true, we stop. This is the destination view controller. If it is false, we continue with the search up the view controller hierarchy, finding the next view controller and performing step 1 again.

A moment’s thought will reveal that the recursive application of this algorithm will eventually arrive at an existing view controller instance with an implementation of the unwind method if there is one. Okay, maybe a moment’s thought didn’t reveal that to you, so here’s an actual example. I’ll use the app whose storyboard is pictured in Figure 6-9. I’ll describe it again, giving the actual names of the view controller classes. Its initial view controller is a UITabBarController with two children:

  • The first tab bar controller child is a UINavigationController with a root view controller called FirstViewController, which has a push segue to another view controller called PushedViewController.

  • The second tab bar controller child is called SecondViewController, which has a modal segue to another view controller called PresentedViewController.

Assume that the user starts in the tab bar controller’s first view controller, where she triggers the push segue, thus showing PushedViewController. She then switches to the tab bar controller’s second view controller, where she triggers the modal segue, thus showing PresentedViewController. All the view controllers pictured in Figure 6-9 now exist simultaneously.

The unwind method is in FirstViewController, and is called iAmFirst(_:). The corresponding unwind segue, whose action is "iAmFirst:", is triggered from a button in PresentedViewController. The user taps the button in PresentedViewController and thus triggers the "iAmFirst:" unwind segue. What will happen?

To begin with, PresentedViewController is sent shouldPerformSegue(withIdentifier:sender:) and returns true, permitting the segue to go forward. The runtime now needs to walk the view controller hierarchy and locate the iAmFirst(_:) method. Here’s how it does that:

  1. We start by walking up the view hierarchy. We thus arrive at the original presenter from which PresentedViewController was presented, namely SecondViewController.

    The runtime sends allowedChildrenForUnwinding(from:) to SecondViewController; SecondViewController has no children, so it returns an empty array.

    So the runtime also asks SecondViewController canPerformUnwindSegueAction to find out whether this is the destination — but SecondViewController returns false, so we know this is not the destination.

  2. We therefore proceed up the view hierarchy to SecondViewController’s parent, the UITabBarController. The runtime sends the UITabBarController allowedChildrenForUnwinding(from:).

    The UITabBarController has two child view controllers, namely the UINavigationController and SecondViewController — but one of them, SecondViewController, contains the source (as it discovers by calling childContaining(_:)). Therefore, the UITabBarController returns an array containing the other child view controller, namely the UINavigationController.

  3. The runtime has received an array with a child in it; it therefore proceeds down the view hierarchy to that child, the UINavigationController, and asks it the same question: allowedChildrenForUnwinding(from:).

    The navigation controller has two children, namely FirstViewController and PushedViewController, and neither of them is or contains the source, so it returns an array containing both of them.

  4. The runtime has received an array with two children in it. It therefore notes down that it now has two hierarchy branches to explore, and proceeds down the hierarchy to explore them:

    1. The runtime starts with PushedViewController, asking it allowedChildrenForUnwinding(from:). PushedViewController has no children, so the reply is an empty array.

      So the runtime asks PushedViewController canPerformUnwindSegueAction to find out whether this is the destination — but PushedViewController replies false, so we know this is not the destination.

    2. So much for that branch of the UINavigationController’s children; we’ve reached a dead end. So the runtime proceeds to the other branch, namely FirstViewController. The runtime asks FirstViewController allowedChildrenForUnwinding(from:). FirstViewController has no children, so the reply is an empty array.

      So the runtime asks FirstViewController canPerformUnwindSegueAction to find out whether this is the destination — and FirstViewController replies true. We’ve found the destination view controller!

The destination having been found, the runtime now sends prepare(for:sender:) to the source, and then calls the destination’s unwind method, iAmFirst(_:). We are now ready to perform the segue.

How an unwind segue is performed

The way an unwind segue is performed is just as ingenious as how the destination is found. During the walk in search of the destination view controller, the runtime remembers the walk. Thus, it knows where all the presented view controllers are, and it knows where all the parent view controllers are. Thus we have a path of presenting view controllers and parent view controllers between the source and the destination. The runtime then proceeds as follows:

  • For any presented view controllers on the path, the runtime itself calls dismiss(animated:completion:) on the presenting view controller.

  • For any parent view controllers on the path, the runtime tells each of them, in turn, to unwind(for:towards:).

The second parameter of unwind(for:towards:) is the direct child of this parent view controller leading down the branch where the destination lives. This child might or might not be the destination, but that’s no concern of this parent view controller. Its job is merely to get us onto that branch, whatever that may mean for this kind of parent view controller. A moment’s thought will reveal (don’t you wish I’d stop saying that?) that if each parent view controller along the path of parent view controllers does this correctly, we will in fact end up at the destination, releasing in good order all intervening view controllers that need to be released. This procedure is called incremental unwind.

Let’s try it! The unwind procedure for our example runs as follows:

  1. The runtime sends dismiss(animated:completion:) to the root view controller, namely the UITabBarController. Thus, PresentedViewController is destroyed in good order.

  2. The runtime sends unwind(for:towards:) to the UITabBarController. The second parameter is the tab bar controller’s first child, the UINavigationController. The UITabBarController therefore changes its selectedViewController to be the UINavigationController.

  3. The runtime sends unwind(for:towards:) to the UINavigationController. The second parameter is the FirstViewController. The navigation controller therefore pops its stack down to the FirstViewController. Thus, PushedViewController is destroyed in good order, and we are back at the FirstViewController — which is exactly what was supposed to happen.

Unwind segue customization

Knowing how an unwind segue works, you can see how to intervene in and customize the process:

  • In a custom view controller that contains an implementation of the unwind method, you might implement canPerformUnwindSegueAction(_:from:withSender:) to return false instead of true so that it doesn’t become the destination on this occasion.

  • In a custom parent view controller, you might implement allowedChildrenForUnwinding(from:). In all probability, your implementation will consist simply of listing your children, calling childContaining(_:) to find out which of your children is or contains the source, subtracting that child from the array, and returning the array — just as the built-in parent view controllers do.

  • In a custom parent view controller, you might implement unwind(for:towards:). The second parameter is one of your current children; you will do whatever it means for this parent view controller to make this the currently displayed child.

In allowedChildrenForUnwinding(from:) and childContaining(_:), the parameter is not a UIStoryboardSegue. It’s an instance of a special value class called UIStoryboardUnwindSegueSource, which has no other job than to communicate, in these two methods, the essential information about the unwind segue needed to make a decision. It has a source, a sender, and an unwindAction (the Selector specified when forming the unwind segue).

Warning

Do not override childContaining(_:). It knows more than you do; you wouldn’t want to interfere with its operation.

View Controller Lifetime Events

As views come and go, driven by view controllers and the actions of the user, events arrive that give your view controller the opportunity to respond to the various stages of its own existence and the management of its view. By overriding these methods, your UIViewController subclass can perform appropriate tasks at appropriate moments. Here’s a list:

viewDidLoad

The view controller has obtained its view (as explained earlier in this chapter); if that involved loading a nib, outlets have been hooked up. This does not mean that the view is in the interface or that it has been given its correct size. You should call super in your implementation, just in case a superclass has work to do in its implementation.

willTransition(to:with:)
viewWillTransition(to:with:)
traitCollectionDidChange(_:)

The view controller’s view is being resized or the trait environment is changing, or both (as explained earlier in this chapter). Your implementation of the first two methods should call super.

updateViewConstraints
viewWillLayoutSubviews
viewDidLayoutSubviews

The view is receiving updateConstraints and layoutSubviews events (as explained earlier in this chapter). Your implementation of updateViewConstraints should call super.

willMove(toParent:)
didMove(toParent:)

The view controller is being added or removed as a child of another view controller (as explained earlier in this chapter). Your implementation of these methods should call super.

viewWillAppear(_:)
viewDidAppear(_:)
viewWillDisappear(_:)
viewDidDisappear(_:)

The view is being added to or removed from the interface. This includes being supplanted by another view controller’s view or being restored through the removal of another view controller’s view. A view that has appeared is in the window; it is part of your app’s active view hierarchy. A view that has disappeared is not in the window; its window is nil. Your implementation of these methods should call super.

To distinguish more precisely why your view is appearing or disappearing, consult any of these properties of the view controller:

  • isBeingPresented

  • isBeingDismissed

  • isMovingtoParent

  • isMovingFromParent

Order of Events

To get a sense for when view controller lifetime events arrive, it helps to examine some specific scenarios in which they normally occur. Take, for example, a UIViewController being pushed onto the stack of a navigation controller. It receives, in this order, the following messages:

  1. willMove(toParent:)

  2. viewWillAppear(_:)

  3. updateViewConstraints

  4. traitCollectionDidChange(_:)

  5. viewWillLayoutSubviews

  6. viewDidLayoutSubviews

  7. viewDidAppear(_:)

  8. didMove(toParent:)

When this same UIViewController is popped off the stack of the navigation controller, it receives, in this order, the following messages:

  1. willMove(toParent:) (with parameter nil)

  2. viewWillDisappear(_:)

  3. viewDidDisappear(_:)

  4. didMove(toParent:) (with parameter nil)

Disappearance, as I mentioned a moment ago, can happen because another view controller’s view supplants this view controller’s view. For example, consider a UIViewController functioning as the top (and visible) view controller of a navigation controller. When another view controller is pushed on top of it, the first view controller gets these messages:

  1. viewWillDisappear(_:)

  2. viewDidDisappear(_:)

The converse is also true. For example, when a view controller is popped from a navigation controller, the view controller that was below it in the stack (the back view controller) receives these messages:

  1. viewWillAppear(_:)

  2. viewDidAppear(_:)

Appear and Disappear Events

The appear and disappear events are particularly appropriate for making sure that a view controller’s view reflects your app’s underlying data each time it appears. These methods are useful also when something must be true exactly so long as a view is in the interface. For example, a repeating Timer that must be running while a view is visible might be started in the view controller’s viewDidAppear(_:) and stopped in its viewWillDisappear(_:). (This architecture also allows you to avoid the retain cycle that could result if you waited to invalidate the timer in a deinit that might otherwise never arrive.)

Changes to the interface performed in viewDidAppear(_:) or viewWillDisappear(_:) may be visible to the user as they occur. If that’s not what you want, use the other member of the pair. For example, in a certain view containing a long scrollable text, I want the scroll position to be the same when the user returns to this view as it was when the user left it, so I save the scroll position in viewWillDisappear(_:) and restore it in viewWillAppear(_:) — not viewDidAppear(_:), where the user might see the scroll position jump.

The appear events are not layout events. Don’t make any assumptions about whether your views have achieved their correct size just because the view is appearing — even if those assumptions seem to be correct. To respond when layout is taking place, implement layout events.

A view does not disappear if a presented view controller’s view merely covers it rather than supplanting it. For example, a view controller that presents another view controller using the .formSheet presentation style gets no lifetime events during presentation and dismissal.

A view does not disappear merely because the app is backgrounded and suspended. Once suspended, your app might be killed. So you cannot rely on viewWillDisappear(_:) or viewDidDisappear(_:) alone for saving data that the app will need the next time it launches. If you are to cover every case, you may need to ensure that your data-saving code also runs in response to an application lifetime event such as applicationWillResignActive or applicationDidEnterBackground.

Warning

Unfortunately, sometimes viewWillAppear(_:) arrives without a corresponding viewDidAppear(_:); similarly, sometimes viewWillDisappear(_:) arrives without a corresponding viewDidDisappear(_:). A case in point is when an interactive transition animation begins and is cancelled. I regard this as a bug.

Event Forwarding to a Child View Controller

A custom container view controller must effectively send willMove(toParent:) and didMove(toParent:) to its children manually, and it will if you do the dance correctly when your view controller acquires or loses a child view controller (see “Container View Controllers”).

A custom container view controller must forward resizing events to its children. This will happen automatically if you call super in your implementation of the willTransition methods.

The appear and disappear events are normally passed along automatically. However, you can take charge by overriding this property:

shouldAutomaticallyForwardAppearanceMethods

If you override this property to return false, you are responsible for seeing that the four appear and disappear methods are called on your view controller’s children. You do not do this by calling these methods directly. The reason is that you have no access to the correct moment for sending them. Instead, you call these two methods on your child view controller:

beginAppearanceTransition(_:animated:)
endAppearanceTransition

The first parameter of the first method is a Bool saying whether this view controller’s view is about to appear (true) or disappear (false).

When your custom container view controller’s own view itself appears or disappears, if its shouldAutomaticallyForwardAppearanceMethods is false, and if it has a child view controller’s view within its own view, it must implement and forward all four appear and disappear events to that child. You’ll need an implementation along these lines, for each of the four events:

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    let child = // whatever
    if child.isViewLoaded && child.view.superview != nil {
        child.beginAppearanceTransition(true, animated: true)
    }
}
override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    let child = // whatever
    if child.isViewLoaded && child.view.superview != nil {
        child.endAppearanceTransition()
    }
}

(The implementations for viewDidAppear(_:) and viewDidDisappear(_:) are similar, except that the first argument for beginAppearanceTransition is false.)

When your custom container view controller swaps one child for another in the interface, if its shouldAutomaticallyForwardAppearanceMethods is false, Apple warns that you should not call the UIViewController transition... method; instead, you perform the transition animation directly, calling beginAppearanceTransition(_:animated:) and endAppearanceTransition yourself. A minimal correct implementation might involve a UIView transition... animation class method (“Transitions”). Here’s an example; I’ve put asterisks to call attention to the additional method calls that forward the appear and disappear events to the children (fromvc and tovc):

self.addChild(tovc) // "will" called for us
fromvc.willMove(toParent: nil)
fromvc.beginAppearanceTransition(false, animated:true) // *
tovc.beginAppearanceTransition(true, animated:true) // *
UIView.transition(
    from:fromvc.view, to:tovc.view,
    duration:0.4, options:.transitionFlipFromLeft) {_ in
        tovc.endAppearanceTransition() // *
        fromvc.endAppearanceTransition() // *
        tovc.didMove(toParent: self)
        fromvc.removeFromParent()
}

View Controller Memory Management

Memory is at a premium on a mobile device. Thus you want to minimize your app’s use of memory. Your motivations are partly altruistic and partly selfish. While your app is running, other apps are suspended in the background; you want to keep your memory usage as low as possible so that those other apps have room to remain suspended and the user can readily switch to them from your app. You also want to prevent your own app from being terminated! If your app is backgrounded and suspended while using a lot of memory, it may be terminated in the background when memory runs short. If your app uses an inordinate amount of memory while in the foreground, it may be summarily killed before the user’s very eyes.

One strategy for avoiding using too much memory is to release any memory-hogging objects you’re retaining if they are not needed at this moment. Because a view controller is the basis of so much of your application’s architecture, it is likely to be a place where you’ll concern yourself with releasing unneeded memory.

One of your view controller’s most memory-intensive objects is its view. Fortunately, the iOS runtime manages a view controller’s view’s memory for you. If a view controller’s view is not in the interface, it can be temporarily dispensed with. In such a situation, if memory is getting tight, then even though the view controller itself persists, and even though it retains its actual view, the runtime may release its view’s backing store (the cached bitmap representing the view’s drawn contents). The view will then be redrawn when and if it is to be shown again later.

If memory runs low, your view controller may be sent this message:

didReceiveMemoryWarning

Sent to a view controller to advise it of a low-memory situation. It is preceded by a call to the app delegate’s applicationDidReceiveMemoryWarning, together with a UIApplication.didReceiveMemoryWarningNotification posted to any registered objects. You are invited to respond by releasing any data that you can do without. Do not release data that you can’t readily and quickly recreate! The documentation advises that you should call super.

To test the behavior of your app under low-memory circumstances, run your app in the Simulator and choose Hardware → Simulate Memory Warning. I don’t believe this has any actual effect on memory, but a memory warning of sufficient severity is sent to your app, so you can see the results of triggering your low-memory response code, including the app delegate’s applicationDidReceiveMemoryWarning and your view controller’s didReceiveMemoryWarning.

Another approach, which works also on a device, is to call an undocumented method. First, define a dummy protocol to make the selector legal:

@objc protocol Dummy {
    func _performMemoryWarning()
}

Now you can send that selector to the shared application:

UIApplication.shared.perform(#selector(Dummy._performMemoryWarning))

(Be sure to remove that code when it is no longer needed for testing, as the App Store won’t accept it.)

There are no hard and fast rules about what might be occupying your app’s memory unnecessarily. Use the Allocations template in Instruments to find out! When multiple view controllers exist simultaneously, interface objects and data in a view controller whose view is not currently visible are obvious candidates for purging when memory is tight. You might also discover that you are retaining large objects that you don’t really need. It will come as no surprise that the most common source of accidental memory bloat is images; a retained array of images, or an image that is much larger than the size at which it needs to be displayed in the interface, can waste a lot of memory.

Lazy Loading

If you’re going to release data in didReceiveMemoryWarning, you must concern yourself with how you’re going to get it back. A simple and reliable mechanism is lazy loading — a getter that reconstructs or fetches the data if it is nil.

For example, suppose we have a property myBigData which might be a big piece of data. We make this a calculated property, storing the real data in a private property (I’ll call it _myBigData). Our calculated property’s setter simply writes through to the private property. In didReceiveMemoryWarning, we write myBigData out as a file (Chapter 22) and set myBigData to nil — thus setting _myBigData to nil as well, and releasing the big data from memory. The getter for myBigData implements lazy loading: if we try to get myBigData when _myBigData is nil, we attempt to fetch the data from the file — and if we succeed, we delete the file (to prevent stale data):

private let fnam = "myBigData"
private var _myBigData : Data! = nil
var myBigData : Data! {
    set (newdata) { self._myBigData = newdata }
    get {
        if _myBigData == nil {
            let fm = FileManager.default
            let f = fm.temporaryDirectory.appendingPathComponent(self.fnam)
            if let d = try? Data(contentsOf:f) {
                self._myBigData = d
                do {
                    try fm.removeItem(at:f)
                } catch {
                    print("Couldn't remove temp file")
                }
            }
        }
        return self._myBigData
    }
}
func saveAndReleaseMyBigData() {
    if let myBigData = self.myBigData {
        let fm = FileManager.default
        let f = fm.temporaryDirectory.appendingPathComponent(self.fnam)
        if let _ = try? myBigData.write(to:f) {
            self.myBigData = nil
        }
    }
}
override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    self.saveAndReleaseMyBigData()
}

NSCache, NSPurgeableData, and Memory-Mapping

When your big data can be reconstructed from scratch on demand, you can take advantage of the built-in NSCache class, which is like a dictionary with the ability to clear out its own entries automatically under memory pressure. As in the previous example, a calculated property can be used as a façade:

private let _cache = NSCache<NSString, NSData>()
var cachedData : Data {
    let key = "somekey" as NSString
    if let olddata = self._cache.object(forKey:key) {
        return olddata as Data
    }
    let newdata = // recreated data
    self._cache.setObject(newdata as NSData, forKey: key)
    return newdata
}

Another built-in class that knows how to clear itself out is NSPurgeableData. It is a subclass of NSMutableData. To signal that the data should be discarded, send your object discardContentIfPossible. Wrap any access to data in calls to beginContentAccess and endContentAccess; the former returns a Bool to indicate whether the data was accessible. The tricky part is getting those access calls right; when you create an NSPurgeableData, you must send it an unbalanced endContentAccess to make its content discardable:

private var _purgeable = NSPurgeableData()
var purgeabledata : Data {
    if self._purgeable.beginContentAccess() && self._purgeable.length > 0 {
        let result = self._purgeable.copy() as! Data
        self._purgeable.endContentAccess()
        return result
    } else {
        let data = // ... recreate data ...
        self._purgeable = NSPurgeableData(data:data)
        self._purgeable.endContentAccess()
        return data
    }
}

(For more about NSCache and NSPurgeableData, see the “Caching and Purgeable Memory” chapter of Apple’s Memory Usage Performance Guidelines in the documentation archive.)

At an even lower level, you can store your data as a file (in some reasonable location such the Caches directory) and read it using the Data initializer init(contentsOfURL:options:) with an options: argument .alwaysMapped. This creates a memory-mapped data object, which has the remarkable feature that it isn’t considered to belong to your memory at all; the system has no hesitation in clearing it from RAM, because it is backed through the virtual memory system by the file, and will be read back into memory automatically when you next access it. This is suitable only for large immutable data, because small data runs the risk of fragmenting a virtual memory page.

Background Memory Usage

You will also wish to concern yourself with releasing memory when your app is about to be suspended. If your app has been backgrounded and suspended and the system later discovers it is running short of memory, it will go hunting through the suspended apps, looking for memory hogs that it can kill in order to free up that memory. If the system decides that your suspended app is a memory hog, it isn’t politely going to wake your app and send it a memory warning; it’s just going to terminate your app in its sleep. The time to be concerned about releasing memory, therefore, is before the app is suspended. You’ll probably want your view controller to be registered with the shared application to receive UIApplication.didEnterBackgroundNotification. The arrival of this notification is an opportunity to release any easily restored memory-hogging objects, such as myBigData in the previous example:

override func viewDidLoad() {
    super.viewDidLoad()
    NotificationCenter.default.addObserver(self,
        selector: #selector(backgrounding),
        name: UIApplication.didEnterBackgroundNotification,
        object: nil)
}
@objc func backgrounding(_ n:Notification) {
    self.saveAndReleaseMyBigData()
}

(In real life, we should be returning from our backgrounding method as quickly as possible; the way to do that is to get onto a background thread and call beginBackgroundTask. I’ll demonstrate in Chapter 24.)

Tip

A nice feature of NSCache is that it evicts its objects automatically when your app goes into the background.

State Restoration

When the user leaves your app and then later returns to it, one of two things might have happened in the meantime:

Your app was suspended

Your app was suspended in the background, and remained suspended while the user did something else. When the user returns to your app, the system simply unfreezes your app, and there it is, looking just as it did when the user left it.

Your app was terminated

Your app was suspended in the background, and then, as the user worked with other apps, a moment came where the system decided it needed the resources (such as memory) being held by your suspended app. Therefore, it terminated your app. When the user returns to your app, the app launches from scratch.

The user, however, doesn’t know the difference between those two things, so why should the app behave differently some of the time? Ideally, your app, when it comes to the foreground, should always appear looking as it did when the user left it, even if in fact it was terminated while suspended in the background. Otherwise, as the WWDC 2013 video on this topic puts it, the user will feel that the app has “lost my place.”

That’s where state restoration comes in. Your app has a state at every moment: some view controller’s view is occupying the screen, and views within it are displaying certain values (for example, a certain switch is set to On, or a certain table view is scrolled to a certain position). The idea of state restoration is to save that information when the app goes into the background, and use it to make all those things true again if the app is subsequently launched from scratch.

iOS provides a general solution to the problem of state restoration. This solution is centered around view controllers, which makes sense, since view controllers are the heart of the problem. What is the user’s “place” in the app, which we don’t want to “lose”? It’s the chain of view controllers that got us to where we were when the app was backgrounded, along with the configuration of each one. The goal of state restoration must therefore be to reconstruct all existing view controllers, initializing each one into the state it previously had.

Note that state, in this sense, is neither user defaults nor data. If something is a preference, store it in UserDefaults. If something is data, keep it in a file (Chapter 22). Don’t misuse the state saving and restoration mechanism for such things. The reason for this is not only conceptual; it’s also because saved state can be lost. (For example, saved state is deleted if the user flicks your app’s snapshot out of the app switcher, or if your app crashes.) You don’t want to commit anything to the state restoration mechanism if it would be a disaster to have lost it the next time the app launches.

How to Test State Restoration

To test whether your app is saving and restoring state as you expect:

  1. Run the app from Xcode as usual, in the Simulator or on a device.

  2. At some point, in the Simulator or on the device, click the Home button (Hardware → Home in the Simulator). This causes the app to be suspended in good order, and state is saved.

  3. Now, back in Xcode, stop the running project and run it again. If there is saved state, it is restored.

(To test the app’s behavior from a truly cold start, delete it from the Simulator or device. You might need to do this, for example, after you’ve changed something about the underlying save-and-restore model.)

Apple also provides some debugging tools (search for “restorationArchiveTool for iOS” at https://developer.apple.com/download/more/):

restorationArchiveTool

A command-line tool letting you examine a saved state archive in textual format. The archive is in a folder called Saved Application State in your app’s sandboxed Library. See Chapter 22 for more about the app’s sandbox and how to copy it to your computer from a device.

StateRestorationDebugLogging.mobileconfig

A configuration profile. When installed on a device, it causes the console to dump information as state saving and restoration proceeds.

StateRestorationDeveloperMode.mobileconfig

A configuration profile. When installed on a device, it prevents the state archive from being jettisoned after unexpected termination of the app (a crash, or manual termination through the app switcher interface). This can allow you to test state restoration a bit more conveniently.

To install a .mobileconfig file on a device, the simplest approach is to email it to yourself on the device and tap the file in the Mail message. You can eventually uninstall the file through the Settings app.

Participating in State Restoration

Built-in state restoration is an opt-in technology: it operates only if you explicitly tell the system that you want to participate in it. To do so, you do three things:

Implement app delegate methods

The app delegate must implement these methods to return true:

  • application(_:shouldSaveApplicationState:)

  • application(_:shouldRestoreApplicationState:)

(Naturally, your code can instead return false to prevent state from being saved or restored on some particular occasion.)

Implement application(_:willFinishLaunchingWithOptions:)

Although it is very early, application(_:didFinishLaunchingWithOptions:) is too late for state restoration. Your app needs its basic interface before state restoration begins. The solution is to use a different app delegate method, application(_:willFinishLaunchingWithOptions:).

Your implementation must call makeKeyAndVisible explicitly on the window! Otherwise, the interface doesn’t come into existence soon enough for restoration to happen during launch. Apart from that, you can typically just reuse your existing application(_:didFinishLaunchingWithOptions:) implementation, by changing did to will in its name.

Provide restoration IDs

Both UIViewController and UIView have a restorationIdentifier property, which is a string. Setting this string to a non-nil value is your signal to the system that you want this view controller (or view) to participate in state restoration. If a view controller’s restorationIdentifier is nil, neither it nor any subsequent view controllers down the chain will be saved or restored. (A nice feature of this architecture is that it lets you participate partially in state restoration, omitting some view controllers by not assigning them a restoration identifier.)

You can set the restorationIdentifier manually, in code; typically you’ll do that early in a view controller’s lifetime. If a view controller or view is instantiated from a nib, you’ll want to set the restoration identifier in the nib editor; the Identity inspector has a Restoration ID field for this purpose. If you’re using a storyboard, it’s a good idea, in general, to make a view controller’s restoration ID in the storyboard the same as its storyboard ID — such a good idea, in fact, that the storyboard editor provides a checkbox, “Use Storyboard ID,” that equates the two values automatically.

(In your application(_:willFinishLaunchingWithOptions:) implementation, before calling makeKeyAndVisible, it may also be useful to assign the window itself a restoration identifier. This might not make any detectable difference, but in some cases it can help restore size class information.)

In the case of a simple storyboard-based app, where each needed view controller instance can be reconstructed directly from the storyboard, those steps alone can be sufficient to bring state restoration to life, operating correctly at the view controller level. Let’s test it! Start with a storyboard-based app with the following architecture (Figure 6-11):

pios 1913
Figure 6-11. Architecture of an app for testing state restoration
  • A navigation controller.

  • Its root view controller, connected by a relationship from the navigation controller. Call its class RootViewController.

    • A presented view controller, connected by a modal segue from a Present button in RootViewController’s view. Call its class PresentedViewController. Its view contains a Dismiss button.

  • A second view controller, connected by a push segue from a Push button in RootViewController’s view. Call its class SecondViewController.

    • The very same presented view controller (PresentedViewController), also connected by a modal segue from a Present button in the second view controller’s view.

This storyboard-based app runs perfectly with just about no code at all; all we need is an empty implementation of an unwind method in RootViewController and SecondViewController so that we can create an unwind segue from the PresentedViewController’s Dismiss button.

We will now make this app implement state restoration:

  1. In the app delegate, change the name of application(_:didFinishLaunchingWithOptions:) to application(_:willFinishLaunchingWithOptions:), and insert this line of code:

    self.window?.makeKeyAndVisible()
  2. In the app delegate, implement these two methods to return true:

    • application(_:shouldSaveApplicationState:)

    • application(_:shouldRestoreApplicationState:)

  3. In the storyboard, give restoration IDs to all four view controller instances: let’s call them "nav", "root", "second", and "presented".

The app now saves and restores state! When we run the app, navigate to any view controller, quit, and later relaunch, the app appears in the same view controller it was in when we quit.

Restoration ID, Identifier Path, and Restoration Class

Having everything done for us by the storyboard reveals nothing about what’s really happening. To learn more, let’s rewrite the example without using the storyboard. Suppose we implement the same architecture using code alone:

// AppDelegate.swift:
func application(_ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions:
    [UIApplication.LaunchOptionsKey : Any]?) -> Bool {
        self.window = self.window ?? UIWindow()
        let rvc = RootViewController()
        let nav = UINavigationController(rootViewController:rvc)
        self.window!.rootViewController = nav
        self.window!.backgroundColor = .white
        self.window!.makeKeyAndVisible()
        return true
}

// RootViewController.swift:
override func viewDidLoad() {
    super.viewDidLoad()
    // ... color view background, create buttons ...
}
@objc func doPresent(_ sender: Any?) {
    let pvc = PresentedViewController()
    self.present(pvc, animated:true)
}
@objc func doPush(_ sender: Any?) {
    let svc = SecondViewController()
    self.navigationController!.pushViewController(svc, animated:true)
}

// SecondViewController.swift:
override func viewDidLoad() {
    super.viewDidLoad()
    // ... color view background, create button ...
}
@objc func doPresent(_ sender: Any?) {
    let pvc = PresentedViewController()
    self.present(pvc, animated:true)
}

// PresentedViewController.m:
override func viewDidLoad() {
    super.viewDidLoad()
    // ... color view background, create button ...
}
@objc func doDismiss(_ sender: Any?) {
    self.presentingViewController?.dismiss(animated:true)
}

That’s a working app. Now let’s start adding state restoration, just as before:

  1. In the app delegate, change the name of application(_:didFinishLaunchingWithOptions:) to application(_:willFinishLaunchingWithOptions:).

  2. In the app delegate, implement these two methods to return true:

    • application(_:shouldSaveApplicationState:)

    • application(_:shouldRestoreApplicationState:)

  3. Give all four view controller instances restoration IDs in code. Again, let’s call them "nav", "root", "second", and "presented". We’re creating each view controller instance manually, so we may as well assign its restorationIdentifier in the next line, like this:

    let rvc = RootViewController()
    rvc.restorationIdentifier = "root"
    let nav = UINavigationController(rootViewController:rvc)
    nav.restorationIdentifier = "nav"

    And so on.

Run the app. Oops! We are not getting state restoration. Why not?

The reason is that the restorationIdentifier alone is not sufficient to tell the state restoration mechanism what to do as the app launches. The restoration mechanism knows the chain of view controller classes that needs to be generated, but it is up to us to generate the instances of those classes. Our storyboard-based example didn’t exhibit this problem, because the storyboard itself was the source of the instances. To make our code-based example work, we need to know about the identifier path and the restoration class.

Identifier path

Any particular view controller instance, given its position in the view controller hierarchy, is uniquely identified by the sequence of restorationIdentifier values of all the view controllers (including itself) in the chain that leads to it. Those restorationIdentifier values, taken together in sequence, constitute the identifier path for any given view controller instance. A view controller’s identifier path is like a trail of breadcrumbs that you left behind as you created it while the app was running, and that will now be used to identify it again as the app launches.

There’s nothing mysterious about an identifier path; it’s just an array of strings. For example, if we launch the app and press the Push button and then the Present button, then all four view controllers have been instantiated; those instances are identified as:

  • The navigation controller: ["nav"]

  • The RootViewController: ["nav","root"]

  • The SecondViewController: ["nav","second"]

  • The PresentedViewController: ["nav","presented"] (because the navigation controller is the actual presenting view controller)

Observe that a view controller’s identifier path is not a record of the full story of how we got here. It’s just an identifier! The state-saving mechanism uses those identifiers to save a relational tree, which does tell the full story. For example, if the app is suspended after we’ve pressed Push followed by Present, then the state-saving mechanism will record the true state of affairs, namely that the root view controller (["nav"]) has two children (["nav","root"] and ["nav","second"]) and a presented view controller (["nav","presented"]).

Now consider what the state restoration mechanism needs to do when the app has been suspended and killed, and comes back to life, from the situation I just described. We need to restore four view controllers; we know their identifiers and mutual relationships. State restoration doesn’t start until after application(_:willFinishLaunchingWithOptions:). So when the state restoration mechanism starts examining the situation, it discovers that the ["nav"] and ["nav","root"] view controller instances have already been created! However, the view controller instances for ["nav","second"] and ["nav","presented"] must also be created now. The state restoration mechanism doesn’t know how to do that — so it’s going to ask your code for the instances.

Restoration class

The state restoration mechanism needs to ask your code for the view controller instances that haven’t been created already. But what code should it ask? There are two ways to specify this. One way is for you to provide a restoration class for each view controller instance that is not restored by the time application(_:willFinishLaunchingWithOptions:) returns. Here’s how you do that:

  1. Give the view controller a restorationClass. Typically, this will be the view controller’s own class, or the class of the view controller responsible for creating this view controller instance. You will need to specify formally that a class to be designated as a restorationClass adopts the UIViewControllerRestoration protocol.

  2. In the restoration class, implement the class method viewController(withRestorationIdentifierPath:coder:), returning a view controller instance. Very often, the implementation will be to instantiate the view controller directly and return that instance. You are handed the identifier path, and can use it to help decide what instance to return.

Let’s make our PresentedViewController and SecondViewController instances restorable. I’ll start with PresentedViewController. Our app can have two PresentedViewController instances (though not simultaneously) — the one created by RootViewController, and the one created by SecondViewController. Let’s start with the one created by RootViewController.

Since RootViewController creates and configures a PresentedViewController instance, it can reasonably act also as the restoration class for that instance. In its implementation of viewController(withRestorationIdentifierPath:coder:), RootViewController should create and configure a PresentedViewController instance exactly as it was doing before we added state restoration to our app — except for putting it into the view controller hierarchy! The state restoration mechanism itself, remember, is responsible for assembling the view controller hierarchy; our job is merely to supply any needed view controller instances.

So RootViewController now must adopt UIViewControllerRestoration, and will contain this code:

func doPresent(_ sender: Any?) {
    let pvc = PresentedViewController()
    pvc.restorationIdentifier = "presented"
    pvc.restorationClass = type(of:self) // *
    self.present(pvc, animated:true)
}
class func viewController(withRestorationIdentifierPath ip: [String],
    coder: NSCoder) -> UIViewController? {
        var vc : UIViewController? = nil
        let last = ip.last!
        switch last {
        case "presented":
            let pvc = PresentedViewController()
            pvc.restorationIdentifier = "presented"
            pvc.restorationClass = self
            vc = pvc
        default: break
        }
        return vc
}

Clearly we now have some annoying code duplication, so let’s factor out the common code. In doing so, we must bear in mind that doPresent is an instance method, whereas viewController(withRestorationIdentifierPath:coder:) is a class method; our factored-out code must therefore be a class method, so that they can both call it:

class func makePresentedViewController () -> UIViewController {
    let pvc = PresentedViewController()
    pvc.restorationIdentifier = "presented"
    pvc.restorationClass = self
    return pvc
}
func doPresent(_ sender: Any?) {
    let pvc = type(of:self).makePresentedViewController()
    self.present(pvc, animated:true)
}
class func viewController(withRestorationIdentifierPath ip: [String],
    coder: NSCoder) -> UIViewController? {
        var vc : UIViewController? = nil
        let last = ip.last!
        switch last {
        case "presented":
            vc = self.makePresentedViewController()
        default: break
        }
        return vc
}

The structure of our viewController(withRestorationIdentifierPath:coder:) is typical. We test the identifier path — usually, it’s sufficient to examine its last element — and return the corresponding view controller; ultimately, we are also prepared to return nil, in case we are called with an identifier path we can’t interpret. We can also return nil deliberately, to tell the restoration mechanism, “Go no further; don’t restore the view controller you’re asking for here, or any view controller further down the same path.”

Continuing in the same vein, we can also make RootViewController the restoration class for SecondViewController, and make SecondViewController the restoration class for the PresentedViewController instance that it creates. There’s no conflict in the notion that both RootViewController and SecondViewController can fulfill the role of PresentedViewController restoration class, as we’re talking about two different PresentedViewController instances. (The details are left as an exercise for the reader.)

The app now performs state saving and restoration correctly!

App delegate instead of restoration class

I said earlier that the state restoration mechanism can ask your code for needed instances in two ways. The second way is that you implement this method in your app delegate:

  • application(_:viewControllerWithRestorationIdentifierPath:coder:)

If you implement that method, it will be called for every view controller that doesn’t have a restoration class. Your job is to create the requested view controller based on its identifier path and return it, or return nil to prevent restoration of that view controller. Be prepared to receive identifier paths for an existing view controller! If that happens, don’t make a new one and don’t return nilreturn the existing view controller.

(The same method works in a storyboard-based app as well, and thus gives you a chance to intervene and prevent the restoration of a particular view controller on a particular occasion by returning nil. )

Restoring View Controller State

I have explained how the state restoration mechanism creates a view controller and places it into the view controller hierarchy. But at that point, the work of restoration is only half done. What about the state of that view controller?

A newly instantiated view controller probably won’t have the data and property values it was holding at the time the app was terminated. The history of the configuration of this view controller throughout the time the app was previously running is not magically recapitulated during restoration. It is up to each view controller to restore its own state when it itself is restored. And in order to do that, it must previously save its own state when the app is backgrounded.

The state saving and restoration mechanism provides a way of helping your view controllers do this, through the use of a coder (an NSCoder object). Think of the coder as a box in which the view controller is invited to place its valuables for safekeeping, and from which it can retrieve them later. Each of these valuables needs to be identified, so it is tagged with a key (an arbitrary string) when it is placed into the box, and is then later retrieved by using the same key, much as in a dictionary.

The Cocoa way to save something into an NSCoder and retrieve it later is that the thing to be saved should be an NSObject subclass that conforms to NSCoding; you can then call the NSCoder instance method encode(_:forKey:). Most Cocoa types meet those requirements, so you can encode a String as an NSString, an Int as an NSNumber, and so forth. Views and view controllers can be safely encoded, because they are treated as references. (I’ll talk in Chapter 22 about how to encode other sorts of object, such as Swift structs.)

Whatever was saved in the coder in this way can later be extracted using the same key by calling decodeObject(of:forKey:). In some cases, you can call a specialized method corresponding to the expected type, such as decodeFloat(forKey:).

The keys do not have to be unique across the entire app; they only need to be unique for a particular view controller. Each object that is handed a coder is handed its own personal coder. It is handed this coder at state saving time, and it is handed the same coder (that is, a coder with the same archived objects and keys) at state restoration time.

Here’s the sequence of events involving coders:

Saving state

When it’s time to save state (as the app is about to be backgrounded), the state saving mechanism provides coders as follows:

  1. The app delegate is sent application(_:shouldSaveApplicationState:). The coder is the second parameter.

  2. The app delegate is sent application(_:willEncodeRestorableStateWith:). The coder is the second parameter, and is the same coder as in the previous step.

  3. Each view controller down the chain, starting at the root view controller, is sent encodeRestorableState(with:). The coder is the parameter. The implementation should call super. Each view controller gets its own coder.

Restoring state

When it’s time to restore state (as the app is launched), the state restoration mechanism provides coders as follows:

  1. The app delegate is sent application(_:shouldRestoreApplicationState:). The coder is the second parameter.

  2. As each view controller down the chain is to be created, one of these methods is called (as I’ve already explained); the coder is the one appropriate to the view controller that’s to be created:

    • The restoration class’s viewController(withRestorationIdentifierPath:coder:), if the view controller has a restoration class.

    • Otherwise, the app delegate’s application(_:viewControllerWithRestorationIdentifierPath:coder:).

  3. Each view controller down the chain, starting at the root view controller, is sent decodeRestorableState(with:). The coder appropriate to that view controller is the parameter. The implementation should call super.

  4. The app delegate is sent application(_:didDecodeRestorableStateWith:). The coder is the second parameter, and is the same one as in the first step.

The UIStateRestoration.h header file describes five built-in keys that are available from every coder during restoration:

UIApplication.stateRestorationViewControllerStoryboardKey

A reference to the storyboard from which this view controller came, if any.

UIApplication.stateRestorationBundleVersionKey

Your Info.plist CFBundleVersion string at the time of state saving.

UIApplication.stateRestorationUserInterfaceIdiomKey

An NSNumber wrapping a UIUserInterfaceIdiom value, either .phone or .pad, telling what kind of device we were running on when state saving happened. You can extract this information as follows:

let key = UIApplication.stateRestorationUserInterfaceIdiomKey
if let raw = coder.decodeObject(of:NSNumber.self, forKey:key) as? Int {
    if let idiom = UIUserInterfaceIdiom(rawValue:raw) {
        if idiom == .phone {
            // ...
        }
    }
}
UIApplication.stateRestorationTimestampKey

A Date telling when state saving happened.

UIApplication.stateRestorationSystemVersionKey

A string telling the system version under which state saving happened.

One purpose of these keys is to allow your app to opt out of state restoration, wholly or in part, because the archive is too old, was saved on the wrong kind of device (and presumably migrated to this one by backup and restore), and so forth.

A typical implementation of encodeRestorableState(with:) and decodeRestorableState(with:) will concern itself with properties and interface views. decodeRestorableState(with:) is guaranteed to be called after viewDidLoad, so you know that viewDidLoad won’t overwrite any direct changes to the interface performed in decodeRestorableState(with:).

To illustrate, I’ll add state saving and restoration to my earlier UIPageViewController example, the one that displays a Pep Boy on each page. Recall how that example is architected. The project has no storyboard. The code defines just two classes, the app delegate and the Pep view controller. The app delegate creates a UIPageViewController and makes it the window’s root view controller, and makes itself the page view controller’s data source; its self.pep instance property holds the data model, which is just an array of string Pep Boy names. The page view controller’s data source methods, pageViewController(_:viewControllerAfter:) and pageViewController(_:viewControllerBefore:), create and supply an appropriate Pep instance whenever an adjacent page is needed for the page view controller, based on the index (in self.pep) of the current Pep page’s boy property.

The challenge is to restore the Pep object displayed in the page view controller as the app launches. One solution involves recognizing that a Pep object is completely configured once created, and it is created just by handing it the name of a Pep Boy in its designated initializer, which becomes its boy property. Thus we can mediate between a Pep object and a mere string, and all we really need to save and restore is that string.

All the additional work, therefore, can be performed in the app delegate. We save and restore the current Pep Boy name in the app delegate’s encode and decode methods:

func application(_ application: UIApplication,
    willEncodeRestorableStateWith coder: NSCoder) {
        let pvc = self.window!.rootViewController as! UIPageViewController
        let boy = (pvc.viewControllers![0] as! Pep).boy
        coder.encode(boy, forKey:"boy")
}
func application(_ application: UIApplication,
    didDecodeRestorableStateWith coder: NSCoder) {
        guard let boy = coder.decodeObject(
            of:NSString.self, forKey:"boy") else {return}
        let pvc = self.window!.rootViewController as! UIPageViewController
        let pep = Pep(pepBoy: boy as String)
        pvc.setViewControllers([pep], direction: .forward, animated: false)
}

A second, more general solution is to make our Pep view controller class itself capable of saving and restoration. This means that every view controller down the chain from the root view controller to our Pep view controller must have a restoration identifier. In our simple app, there’s just one such view controller, the UIPageViewController; the app delegate can assign it a restoration ID when it creates it:

let pvc = UIPageViewController(
    transitionStyle: .scroll, navigationOrientation: .horizontal)
pvc.restorationIdentifier = "pvc" // *

We’ll have a Pep object assign itself a restoration ID in its own designated initializer. The Pep object will also need a restoration class; as I said earlier, this can perfectly well be the Pep class itself, and that seems most appropriate here:

required init(pepBoy boy:String) { // *
    self.boy = boy
    super.init(nibName: nil, bundle: nil)
    self.restorationIdentifier = "pep" // *
    self.restorationClass = type(of:self) // *
}

The only state that a Pep object needs to save is its boy string, so we implement encodeRestorableState to do that. We don’t need to implement decodeRestorableState, because the coder that will come back to us in viewController(withRestorationIdentifierPath:coder:) contains the boy string, and once we use it to create the Pep instance, the Pep instance is completely configured. This is a class method, and it can’t call an initializer on self unless that initializer is marked as required; we did mark it required (in the previous code):

override func encodeRestorableState(with coder: NSCoder) {
    super.encodeRestorableState(with:coder)
    coder.encode(self.boy, forKey:"boy")
}
class func viewController(withRestorationIdentifierPath ip: [String],
    coder: NSCoder) -> UIViewController? {
        if let boy = coder.decodeObject(of:NSString.self, forKey:"boy") {
            return self.init(pepBoy: boy as String)
        } else {
            return nil
        }
}

Now comes a surprise. We run the app and test it, and we find that we’re not getting saving and restoration of our Pep object. It isn’t being archived; its encodeRestorableState(with:) isn’t even being called! The reason is that the state saving mechanism doesn’t work automatically for a UIPageViewController and its children (or for a custom container view controller and its children, for that matter). It is up to us to see to it that the current Pep object is archived.

To do so, we can archive and unarchive the current Pep object in an implementation of encodeRestorableState(with:) and decodeRestorableState(with:) that is being called. For our app, that would have to be in the app delegate. The code we’ve written so far has all been necessary to make the current Pep object archivable and restorable; now the app delegate will make sure that it is archived and restored:

func application(_ application: UIApplication,
    willEncodeRestorableStateWith coder: NSCoder) {
        let pvc = self.window!.rootViewController as! UIPageViewController
        let pep = pvc.viewControllers![0] as! Pep
        coder.encode(pep, forKey:"pep")
}
func application(_ application: UIApplication,
    didDecodeRestorableStateWith coder: NSCoder) {
        guard let pep = coder.decodeObject(
            of:Pep.self, forKey:"pep") else {return}
        let pvc = self.window!.rootViewController as! UIPageViewController
        pvc.setViewControllers([pep], direction: .forward, animated: false)
}

This solution may seem rather heavyweight, but it isn’t. We’re not really archiving an entire Pep instance; it’s just a reference. The Pep instance that arrives in application(_:didDecodeRestorableStateWith:) was never in the archive; it’s just a pointer to the instance created by Pep’s implementation of viewController(withRestorationIdentifierPath:coder:).

Restoration Order of Operations

When you implement state saving and restoration for a view controller, the view controller ends up with two different ways of being configured. One way involves the view controller lifetime events I discussed earlier (“View Controller Lifetime Events”). The other involves the state restoration events I’ve been discussing here. You want your view controller to be correctly configured regardless of whether this view controller is undergoing state restoration or not.

To help you with this, there’s another view controller event I haven’t mentioned yet: applicationFinishedRestoringState. If you implement this method in a view controller subclass, it will be called if and only if we’re doing state restoration, at a time when all view controllers have already been sent decodeRestorableState(with:).

Thus, the known order of events during state restoration is like this:

  1. application(_:shouldRestoreApplicationState:)

  2. application(_:viewControllerWithRestorationIdentifierPath:coder:)

  3. viewController(withRestorationIdentifierPath:coder:), in order down the chain

  4. viewDidLoad, in order down the chain, possibly interleaved with the foregoing

  5. decodeRestorableState(with:), in order down the chain

  6. application(_:didDecodeRestorableStateWith:)

  7. applicationFinishedRestoringState, in order down the chain

Observe that I’ve said nothing about when viewWillAppear(_:) and viewDidAppear(_:) will arrive. You can’t be sure about this, or even whether viewDidAppear(_:) will arrive at all. But in applicationFinishedRestoringState you can reliably finish configuring your view controller and your interface.

A typical situation is that you will want to update your interface after all properties have been set. So you’ll factor out your interface-updating code into a single method. Now there are two possibilities, and they are both handled coherently:

We’re not restoring state

Properties will be set through initialization and configuration, and then you’ll call your interface-updating method. All this could happen as soon as the end of viewDidLoad.

We are restoring state

Properties will be set by decodeRestorableState(with:), and then applicationFinishedRestoringState calls your interface-updating method.

There is still some indeterminacy as to what’s going to happen, but the interface-updating method can mediate that indeterminacy by checking for two things that can go wrong:

It is called too soon

The interface-updating method should check to see that the properties that it needs to consult have in fact been set; if not, it should just return. It will be called again when the properties have been set.

It is called unnecessarily

The interface-updating method might run twice in quick succession with the same set of properties. This is not a disaster, but if you don’t like it, you can prevent it: compare the properties to the interface, and return if the interface has already been configured with them.

In this simple example, our view controller has a boy property, and its interface configuration consists of displaying the corresponding Pep boy’s image in an image view. So we factor out the construction of the initial interface into a method, finishInterface, which starts by checking whether boy has been set:

var boy : String?
func finishInterface() {
    if let boy = self.boy {
        let im = UIImageView(image: UIImage(named:boy.lowercased()))
        self.view.addSubview(im)
        // ... position the image view ...
    }
}

If we are launched without state restoration, boy is set by whoever creates this view controller, before viewDidLoad. Thus, when we call finishInterface from viewDidLoad, self.boy has been set and the image view is created:

override func viewDidLoad() {
    super.viewDidLoad()
    self.finishInterface()
}

But if we are launched with state restoration, boy is not set when viewDidLoad runs, and the call to finishInterface does nothing, because self.boy is nil. Now restoration continues:

override func decodeRestorableState(with coder: NSCoder) {
    if let boy = coder.decodeObject(of:NSString.self, forKey: "boy") {
        self.boy = boy as String
    }
}
override func encodeRestorableState(with coder: NSCoder) {
    coder.encode(self.boy, forKey: "boy")
}
override func applicationFinishedRestoringState() {
    self.finishInterface()
}

Our applicationFinishedRestoringState implementation calls finishInterface again. But this time, decodeRestorableState has been called, and self.boy has been set — so now finishInterface finishes the interface by creating the image view. In this way, the image view is added to the interface just once, no matter what.

If your app has additional state restoration work to do on a background thread (Chapter 24), the documentation says you should call UIApplication’s extendStateRestoration as you begin and completeStateRestoration when you’ve finished. The idea is that if you don’t call completeStateRestoration, the system can assume that something has gone wrong and will throw away the saved state information in case it is faulty.

Restoration of Other Objects

A view will participate in automatic saving and restoration of state if its view controller does, and if it itself has a restoration identifier. Some built-in UIView subclasses have built-in restoration abilities. For example, a scroll view that participates in state saving and restoration will automatically return to the point to which it was scrolled previously. You should consult the documentation on each UIView subclass to see whether it participates usefully in state saving and restoration, and I’ll mention a few significant cases when we come to discuss those views in later chapters.

In addition, an arbitrary object can be made to participate in automatic saving and restoration of state. There are three requirements for such an object:

  • The object’s class must be an NSObject subclass adopting the UIStateRestoring protocol. This protocol declares three optional methods:

    • encodeRestorableState(with:)

    • decodeRestorableState(with:)

    • applicationFinishedRestoringState

  • When the object is created, someone must register it with the runtime by calling this UIApplication class method:

    • registerObject(forStateRestoration:restorationIdentifier:)

  • Someone who participates in state saving and restoration, such as a view controller, must make the archive aware of this object by storing a reference to it in the archive (typically in encodeRestorableState(with:)) — much as we did with the Pep object earlier.

So, for example, here’s an NSObject subclass Thing with a word property, that participates in state saving and restoration:

class Thing : NSObject, UIStateRestoring {
    var word = ""
    func encodeRestorableState(with coder: NSCoder) {
        coder.encode(self.word, forKey:"word")
    }
    func decodeRestorableState(with coder: NSCoder) {
        if let word = coder.decodeObject(of: NSString.self, forKey:"word") {
            self.word = word as String
        }
    }
    func applicationFinishedRestoringState() {
        // not used
    }
}

And here’s a view controller with an Optional Thing property (self.thing):

class func makeThing () -> Thing {
    let thing = Thing()
    UIApplication.registerObject(
        forStateRestoration: thing, restorationIdentifier: "thing")
    return thing
}
override func awakeFromNib() {
    super.awakeFromNib()
    self.thing = type(of:self).makeThing()
}
override func encodeRestorableState(with coder: NSCoder) {
    super.encodeRestorableState(with:coder)
    coder.encode(self.thing, forKey: "mything") // *
}

The starred line is crucial; it introduces our Thing object to the archive and brings its UIStateRestoring methods to life. The result is that if we background the app while an instance of this view controller exists, and if state restoration is performed on the next launch, the view controller’s Thing has the same word that it had before; the Thing has participated in state saving and restoration along with the view controller that owns it.

There is an optional objectRestorationClass property of the restorable object, and an object(withRestorationIdentifierPath:coder:) class method that the restoration class must implement. The class in question should formally adopt UIObjectRestoration. Its object(withRestorationIdentifierPath:coder:) should return the restorable object, by creating it or pointing to it; alternatively, it can return nil to prevent restoration. If you want to assign an objectRestorationClass, you’ll have to declare the property:

var objectRestorationClass: UIObjectRestoration.Type?

However, our Thing object was restorable even without an objectRestorationClass; presumably, just calling registerObject sufficiently identifies this object to the runtime.

Another optional property of the restorable object is restorationParent. Again, if you want to assign to it, you’ll have to declare it:

var restorationParent: UIStateRestoring?

The purpose of the restoration parent is to give the restorable object an identifier path. For example, if we have a chain of view controllers with a path ["nav","second"], then if that last view controller is the restorationParent of our Thing object, the Thing object’s identifier path in object(withRestorationIdentifierPath:coder:) will be ["nav","second","thing"], rather than simply ["thing"]. This is useful if we are worried that ["thing"] alone will not uniquely identify this object.

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

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