Coordinating Efforts Between View Controllers

Few iOS apps show only a single screenful of content. Instead, they show some content when first launched and then show and hide other content in response to user actions. These transitions provide a single unified user interface that display a lot of content, just not all at once.

By convention, smaller pieces of content are managed by different view controller classes. This coding convention allows you to create smaller and simpler controller classes that are easy to implement. However, dividing the work between multiple classes imposes additional requirements on your class designs. To maintain the illusion of a single interface, your view controllers must exchange messages and data to coordinate transitions from controller to another. Thus, even as your view controller classes look inwards to control views and perform the tasks assigned to them, they also look outwards to communicate with other collaborating view controllers.

When Coordination Between View Controllers Occurs

Communication between view controllers is tied to the role those view controllers play in your app. It would be impossible to describe all of the possible interactions between view controllers, because the number and nature of these relationships is dependent on the design of your app. However, it is possible to describe when these interactions occur and to give some examples of the kinds of coordination that might take place in your app.

The lifetime of a view controller has three stages during which it might coordinate with other objects:

View controller instantiation. In this stage, when a view controller is created, an existing view controller or another object was responsible for its creation. Usually, this object knows why the view controller was created and what task it should perform. Thus, after a view controller is instantiated, this intent must be communicated to it.

The exact details of this initial configuration vary. Sometimes, the existing view controller passes data objects to the new controller. At other times, it may configure the presentation style for that, or establish lasting links between the two view controllers. These links allow further communication later in the view controller’s lifetime.

During the view controller’s lifetime. In this stage, some view controllers communicate with other view controllers during their lifetime. The recipient of these messages could be the view controller that created it, peers with similar lifetimes, or even a new view controller that it itself created. Here are a few common designs:

View controller destruction. In this stage, many view controllers send messages when their task completes. These messages are common because the convention is for the controller that created a view controller to also release it. Sometimes, these messages simply convey that the user finished the task. At other times, such as when the task being performed generated new data objects, the message communicates the new data back to another controller.

During a view controller’s lifetime, it is common for it to exchange information with other view controllers. These messages are used to notify other controllers when things happen, send them data, or even ask them to exert control over the controller’s activities.

With Storyboards, a View Controller is Configured When It Is Instantiated

Storyboards provide direct support for configuring newly instantiated controllers before they are displayed. When a storyboard instantiates new view controllers automatically, it calls an object in your app to allow it to configure the new controller or to create links to or from the new controller. When your app first launches, the app delegate configures the initial view controller. When a segue is triggered, the source view controller configures the destination view controller.

There are a few conventions used to implement destination view controllers:

Carefully following these conventions helps organize your configuration code and carefully limits the direction of dependencies between view controller classes in your app. By isolating dependencies in your app, you increase the opportunity for code reuse. You also design view controllers that are easier to test in isolation from the rest of your app.

Configuring the Initial View Controller at Launch

If you define a main storyboard in your project, iOS automatically does a lot of work for you to set up your app. When your app calls the UIApplicationMain function, iOS performs the following actions:

  1. It instantiates the app delegate based on the class name you passed into the UIApplicationMain function.

  2. It creates a new window attached to the main screen.

  3. If your app delegate implements a window property, iOS sets this property to the new window.

  4. It loads the main storyboard referenced in the app’s information property list file.

  5. It instantiates the main storyboard’s initial view controller.

  6. It sets the window’s rootViewController property to the new view controller.

  7. It calls the app delegate’s application:didFinishLaunchingWithOptions: method. Your app delegate is expected to configure the initial view controller (and its children, if it is a container view controller).

  8. It calls the window’s makeKeyAndVisible method to display the window.

Listing 11-1 shows an implementation of the application:didFinishLaunchingWithOptions: method from the Your Second iOS App: Storyboards tutorial. In this example, the storyboard’s initial view controller is a navigation controller with a custom content controller that displays the master view. The code first retrieves references to the view controller it is interested in. Then, it performs any configuration that could not be performed in Interface Builder. In this example, a custom data controller object is provided to the master view controller by a custom data controller object.

Listing 11-1  The app delegate configures the controller

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    UINavigationController *navigationController = (UINavigationController*) self.window.rootViewController;
    BirdsMasterViewController * firstViewController = [[navigationController viewControllers] objectAtIndex:0];
 
    BirdSightingDataController *dataController = [[BirdSightingDataController alloc] init];
    firstViewController.dataController = dataController;
 
    return YES;
}

If your project does not identify the main storyboard, the UIApplicationMain function creates the app delegate and calls it but does not perform any of the other steps described earlier. You would need to write code to perform those steps yourself. Listing 11-2 shows the code you might implement if you needed to perform these steps programmatically.

Listing 11-2  Creating the window when a main storyboard is not being used

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"MyStoryboard" bundle:nil];
    MainViewController *mainViewController = [storyboard instantiateInitialViewController];
    self.window.rootViewController = mainViewController;
 
    // Code to configure the view controller goes here.
 
    [self.window makeKeyAndVisible];
    return YES;
}

Configuring the Destination Controller When a Segue is Triggered

iOS performs the following tasks when a segue is triggered:

  1. It instantiates the destination view controller.

  2. It instantiates a new segue object that holds all the information for the segue being triggered.

  3. It calls the source view controller’s prepareForSegue:sender: method, passing in the new segue object and the object that triggered the segue.

  4. It calls the segue’s perform method to bring the destination controller onto the screen. The actual behavior depends on the kind of segue being performed. For example, a modal segue tells the source view controller to present the destination view controller.

  5. It releases the segue object and the segue is complete.

The source view controller’s prepareForSegue:sender: method performs any necessary configuration of the destination view controller’s properties, including a delegate if the destination view controller implements one.

Listing 11-3 shows an implementation of the prepareForSegue:sender: method from the Your Second iOS App: Storyboards tutorial.

Listing 11-3  Configuring the destination controller in a segue

- (void) prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    if ([[segue identifier] isEqualToString:@"ShowSightingsDetails"])
    {
        DetailViewController *detailViewController = [segue destinationViewController];
        detailViewController.sighting = [self.dataController objectInListAtIndex:[self.tableView indexPathForSelectedRow].row];
    }
 
    if ([[segue identifier] isEqualToString:@"ShowAddSightingView"])
    {
        AddSightingViewController *addSightingViewController = [[[segue destinationViewController] viewControllers] objectAtIndex:0];
        addSightingViewController.delegate = self;
    }
}

This implementation, from the master view controller for the app, actually handles two different segues configured in the storyboard. It distinguishes between the two segues using the segue’s identifier property. In both cases, it follows the coding convention established earlier, by first retrieving the view controller and then configuring it.

When the segue is to the detail view controller, the segue occurred because the user selected a row in the table view. In this case, the code transfers enough data to the destination view controller so that the destination view controller can display the sighting. The code uses the user’s selection to retrieve a sighting object from the master view controller’s data controller. It then assigns this sighting to the destination controller.

In the other case, the new view controller allows the user to add a new bird sighting. No data needs to be sent to this view controller. However, the master view controller needs to receive data when the user finishes entering the data. To receive that information, the source view controller implements the delegate protocol defined by the Add view controller (not shown here) and makes itself the destination view controller’s delegate.

Using Delegation to Communicate with Other Controllers

In a delegate-based model, the view controller defines a protocol for its delegate to implement. The protocol defines methods that are called by the view controller in response to specific actions, such as taps in a Done button. The delegate is then responsible for implementing these methods. For example, when a presented view controller finishes its task, it sends a message to the presenting view controller and that controller dismisses it.

Using delegation to manage interactions with other app objects has key advantages over other techniques:

To illustrate the implementation of a delegate protocol, consider the recipe view controller example that was used in “Presenting a View Controller and Choosing a Transition Style.” In that example, a recipes app presented a view controller in response to the user wanting to add a new recipe. Prior to presenting the view controller, the current view controller made itself the delegate of the RecipeAddViewController object. Listing 11-4 shows the definition of the delegate protocol for RecipeAddViewController objects.

Listing 11-4  Delegate protocol for dismissing a presented view controller

@protocol RecipeAddDelegate <NSObject>
// recipe == nil on cancel
- (void)recipeAddViewController:(RecipeAddViewController *)recipeAddViewController
                   didAddRecipe:(MyRecipe *)recipe;
@end

When the user taps the Cancel or Done button in the new recipe interface, the RecipeAddViewController object calls the preceding method on its delegate object. The delegate is then responsible for deciding what course of action to take.

Listing 11-5 shows the implementation of the delegate method that handles the addition of new recipes. This method is implemented by the view controller that presented the RecipeAddViewController object. If the user accepted the new recipe—that is, the recipe object is not nil—this method adds the recipe to its internal data structures and tells its table view to refresh itself. (The table view subsequently reloads the recipe data from the same recipesController object shown here.) Then the delegate method dismisses the presented view controller.

Listing 11-5  Dismissing a presented view controller using a delegate

- (void)recipeAddViewController:(RecipeAddViewController *)recipeAddViewController
                   didAddRecipe:(Recipe *)recipe {
   if (recipe) {
      // Add the recipe to the recipes controller.
      int recipeCount = [recipesController countOfRecipes];
      UITableView *tableView = [self tableView];
      [recipesController insertObject:recipe inRecipesAtIndex:recipeCount];
 
      [tableView reloadData];
   }
   [self dismissViewControllerAnimated:YES completion: nil];
}

Guidelines for Managing View Controller Data

Carefully managing how data and control flows between your view controllers is critical to understanding how your app operates and avoiding subtle errors. Consider the following guidelines when designing your view controllers: