Creating Custom Container View Controllers

Container view controllers are a critical part of iOS app design. They allow you to decompose your app into smaller and simpler parts, each controlled by a view controller dedicated to that task. Containers allow these view controllers to work together to present a seamless interface.

iOS provides many standard containers to help you organize your apps. However, sometimes you need to create a custom workflow that doesn’t match that provided by any of the system containers. Perhaps in your vision, your app needs a specific organization of child view controllers with specialized navigation gestures or animation transitions between them. To do that, you implement a custom container.

Designing Your Container View Controller

In most ways, a container view controller is just like a content view controller. It manages views and content, coordinates with other objects in your app, and responds to events in the responder chain. Before designing a container controller, you should already be familiar with designing content view controllers. The design questions in “Creating Custom Content View Controllers” also apply when creating containers.

When you design a container, you create explicit parent-child relationships between your container, the parent, and other view controllers, its children. More specifically, Figure 14-1 shows that there are explicit connections between the views as well. Your container adds the content views of other view controllers in its own view hierarchy. Whenever a child’s view is displayed in the container’s view hierarchy, your container also establishes a connection to the child view controller and ensures that all appropriate view controller events are sent to the child.

Figure 14-1  A container view controller’s view hierarchy contains another controller’s views

Your container should make the rules and its children should follow them; it is up to the parent to decide when a child’s content is visible in its own view hierarchy. The container decides where in the hierarchy that view is placed and how it is sized and positioned there. This design principle is no different from that of a content view controller. The view controller is responsible for managing its own view hierarchy and other classes should never manipulate its contents. Where necessary, your container class can expose public methods and properties to allow its behavior to be controlled. For example, if another object needs to be able to tell your container to display a new view, then your container class should expose a public method to allow this transition to occur. The actual implementation that changes the view hierarchy should be in the container class. This guiding principle cleanly separates responsibilities between the container and its children by always making each view controller responsible for its own view hierarchy.

Here are some specific questions you should be able to answer about your container class:

In summary, a container controller often has more relationships with other objects (especially other view controllers) than a content controller. So, you need to put additional effort into understanding how the container works. Ideally, as with a content controller, you want to hide many of those behaviors behind an excellent public class API.

Examples of Common Container Designs

The easiest way to understand how to design a new container class is to examine the behavior and public API of the existing system container classes. Each defines its own navigation metaphor and a programming interface used to configure it. This section takes a look at a few of these classes from the viewpoint of container design. It does not provide a complete description of each class’s programming interface, but just looks at some of the critical concepts. For detailed information about using these system containers, see View Controller Catalog for iOS.

A Navigation Controller Manages a Stack of Child View Controllers

A navigation controller allows a sequence of distinct user interface screens to be displayed to the user. The metaphor used by a navigation controller is a stack of child view controllers. The topmost view controller’s view is placed in the navigation controller’s view hierarchy. To display a new view controller, you push it onto the stack. When you are done, you remove the view controller from the stack.

Figure 14-2 shows that only a single child’s view is visible and that the child’s view is part of a more complex hierarchy of views provided by the navigation controller.

Figure 14-2  A navigation controller’s view and view controller hierarchy

When a view controller is pushed onto or popped from the stack, the transition can be animated, which means the views of two children are briefly displayed together. In addition to the child views, a navigation controller also includes its own content views to display a navigation bar. The contents of the navigation bar are updated based on the child being displayed.

Here are some of the important methods and properties that the UINavigationController class uses to define its behavior:

  • The topViewController property states which controller is at the top of the stack.

  • The viewControllers property lists all the children in the stack.

  • The pushViewController:animated: method pushes a new view controller on the stack. This method does all the work necessary to update the view hierarchy to display the new child’s view.

  • The popViewControllerAnimated: method removes the top view controller from the stack.

  • The delegate property allows a client of the container to be notified when state transitions occur.

The navigation controller uses properties on the child view controller to adjust the content it displays. These properties are defined by UIViewController base class so that some default behavior is available; this allows any view controller to be made a child of a navigation controller. Here are some of the properties the navigation controller looks for:

  • The navigationItem property provides the contents of the navigation toolbar.

  • The toolbarItems property provides the contents of the bottom bar.

  • The editButtonItem property provides access to a view in the navigation item so that the navigation controller can toggle the child view’s edit mode.

A Tab Bar Controller Uses a Collection of Child Controllers

A tab view controller allows a set of distinct user interface screens to be displayed to the user. However, instead of a stack of view controllers, a tab view controller uses a simple array. Figure 14-3 shows that again, only one child view controller’s view is displayed at a time. However, these views do not need to be accessed sequentially, and the transition to the new child is usually not animated.

Figure 14-3  A tab bar controller’s view and view controller hierarchy

Here are some of the important methods and properties that UITabBarController class uses to allow apps to control what a tab bar controller displays:

  • The viewControllers property holds the list of child view controllers that act as the tabs of content.

  • The selectedViewController property allows you to read or change which child is visible.

  • The delegate property allows a client of the container to be notified when state transitions occur.

A tab bar controller uses the child’s tabBarItem property to determine how it is displayed in the appropriate tab.

A Page Controller Uses a Data Source to Provide New Children

A page controller uses pages of content as its metaphor, like the pages of a book. Each page displayed by the container is provided by a child view controller.

Books can have many pages—far more than the number of screens of content in a navigation controller—so keeping all the pages in memory at once may not be possible. Instead, the page view controller keeps child controllers for the visible pages and fetches other pages on demand. When the user wants to see a new page, the container calls the object associated with its dataSource property to get the new controller. Thus, a page view controller using a data source uses a pull model rather than having your app directly push new pages onto itself.

A page view controller can also be customized for different kinds of book layouts. The number of pages and the size of the pages can differ. Here are two key properties that affect the page view controller’s behavior:

  • The spineLocation property determines how the pages are organized. Some layouts only display one page at a time. Other layouts display multiple pages.

  • The transitionStyle property determines how transitions between pages are animated.

Implementing a Custom Container View Controller

Once you’ve designed your class’s behavior and determined many aspects of its public API, you are ready to start implementing the container. The goal of implementing a container is to be able to add another view controller’s view (and associated view hierarchy) as a subtree in your container’s view hierarchy. The child remains responsible for its own view hierarchy, save for where the container decides to place it onscreen. When you add the child’s view, you need to ensure that events continue to be distributed to both view controllers. You do this by explicitly associating the new view controller as a child of the container.

The UIViewController class provides methods that a container view controller uses to manage the relationship between itself and its children. The complete list of methods and properties is in the reference; see “Managing Child View Controllers in a Custom Container” in UIViewController Class Reference

Adding and Removing a Child

Listing 14-1 shows a typical implementation that adds a view controller as a child of another view controller. Each numbered step in the listing is described in more detail following the listing.

Listing 14-1  Adding another view controller’s view to the container’s view hierarchy

- (void) displayContentController: (UIViewController*) content;
{
   [self addChildViewController:content];                 // 1
   content.view.frame = [self frameForContentController]; // 2
   [self.view addSubview:self.currentClientView];
   [content didMoveToParentViewController:self];          // 3
}

Here’s what the code does:

  1. It calls the container’s addChildViewController: method to add the child. Calling the addChildViewController: method also calls the child’s willMoveToParentViewController: method automatically.

  2. It accesses the child’s view property to retrieve the view and adds it to its own view hierarchy. The container sets the child’s size and position before adding the view; containers always choose where the child’s content appears. Although this example does this by explicitly setting the frame, you could also use layout constraints to determine the view’s position.

  3. It explicitly calls the child’s didMoveToParentViewController: method to signal that the operation is complete.

Eventually, you want to be able to remove the child’s view from the view hierarchy. In this case, shown in Listing 14-2, you perform the steps in reverse.

Listing 14-2  Removing another view controller’s view to the container’s view hierarchy

- (void) hideContentController: (UIViewController*) content
{
   [content willMoveToParentViewController:nil];  // 1
   [content.view removeFromSuperview];            // 2
   [content removeFromParentViewController];      // 3
}

Here’s what this code does:

  1. Calls the child’s willMoveToParentViewController: method with a parameter of nil to tell the child that it is being removed.

  2. Cleans up the view hierarchy.

  3. Calls the child’s removeFromParentViewController method to remove it from the container. Calling the removeFromParentViewController method automatically calls the child’s didMoveToParentViewController: method.

For a container with essentially static content, adding and removing view controllers is as simple as that. Whenever you want to add a new view, add the new view controller as a child first. After the view is removed, remove the child from the container. However, sometimes you want to animate a new child onto the screen while simultaneously removing another child. Listing 14-3 shows an example of how to do this.

Listing 14-3  Transitioning between two view controllers

- (void) cycleFromViewController: (UIViewController*) oldC
            toViewController: (UIViewController*) newC
{
    [oldC willMoveToParentViewController:nil];                        // 1
    [self addChildViewController:newC];
 
    newC.view.frame = [self newViewStartFrame];                       // 2
    CGRect endFrame = [self oldViewEndFrame];
 
    [self transitionFromViewController: oldC toViewController: newC   // 3
          duration: 0.25 options:0
          animations:^{
             newC.view.frame = oldC.view.frame;                       // 4
             oldC.view.frame = endFrame;
           }
           completion:^(BOOL finished) {
             [oldC removeFromParentViewController];                   // 5
             [newC didMoveToParentViewController:self];
            }];
}

Here’s what this code does:

  1. Starts both view controller transitions.

  2. Calculates two new frame positions used to perform the transition animation.

  3. Calls the transitionFromViewController:toViewController:duration:options:animations:completion: method to perform the swap. This method automatically adds the new view, performs the animation, and then removes the old view.

  4. The animation step to perform to get the views swapped.

  5. When the transition completes, the view hierarchy is in its final state, so it finishes the operation by sending the final two notifications.

Customizing Appearance and Rotation Callback Behavior

Once you add a child to a container, the container automatically forwards rotation and appearance callbacks to the child view controllers as soon as an event occurs that requires the message to be forwarded. This is normally the behavior you want, because it ensures that all events are properly sent. However, sometimes the default behavior may send those events in an order that doesn’t make sense for your container. For example, if multiple children are simultaneously changing their view state, you may want to consolidate the changes so that the appearance callbacks all happen at the same time in a more logical order. To do this, you modify your container class to take over responsibility for appearance or rotation callbacks.

To take over control of appearance callbacks, you override the shouldAutomaticallyForwardAppearanceMethods method to return NO. Listing 14-4 shows the necessary code.

Listing 14-4  Disabling automatic appearance forwarding

- (BOOL) shouldAutomaticallyForwardAppearanceMethods
{
    return NO;
}

To actually inform the child view controller that an appearance transition is occurring, you call the child’s beginAppearanceTransition:animated: and endAppearanceTransition methods.

If you take over sending these messages, you are also responsible for forwarding them to children when your container view controller appears and disappears. For example, if your container has a single child referenced by a child property, your container would forward these messages to the child, as shown in Listing 14-5.

Listing 14-5  Forwarding appearance messages when the container appears or disappears

-(void) viewWillAppear:(BOOL)animated
{
    [self.child beginAppearanceTransition: YES animated: animated];
}
 
-(void) viewDidAppear:(BOOL)animated
{
    [self.child endAppearanceTransition];
}
 
-(void) viewWillDisappear:(BOOL)animated
{
    [self.child beginAppearanceTransition: NO animated: animated];
}
 
-(void) viewDidDisappear:(BOOL)animated
{
    [self.child endAppearanceTransition];
}

Forwarding rotation events works almost identically and can be done independently of forwarding appearance messages. First, you override the shouldAutomaticallyForwardRotationMethods method to return NO. Then, at times appropriate to your container, you call the following methods:

Practical Suggestions for Building a Container View Controller

Designing, developing, and testing a new container view controller takes time. Although the individual behaviors are straightforward, the controller as a whole can be quite complex. Consider some of the following guidance when implementing your own container classes: