Storyboards, View Controller Containment and Delegation: Coordinating Responsibilities Within an App / by Jake MacMullin

This year marks the 100th anniversary of Australia's capital city Canberra. During this centenary year CSIRO, Australia's peak scientific research body, wanted to reflect on their history in Canberra with an iPad and iPhone app which they asked Stripy Sock to create for them.

The app (available free from the app store) includes stories that can be viewed on a map or on a timeline. It is also a universal app with both an iPhone and iPad version. While both versions allow you to see the timeline and the map, they do so in different ways. On the iPad both the map and the timeline are presented side by side while on the iPhone you can switch between a timeline or map tab.

 

The iPad app showing a map and timeline

This post describes how I used storyboards, view controller containment and delegation so that these map and timeline components could be used in these different ways in both the iPhone and iPad apps.

As I knew that I'd be displaying stories on a map in both versions of the app, I wanted to make sure that any code I wrote in the first version I created (which was the iPad app) could be reused in the next version. But I also knew I wanted different behaviour in each version. Selecting a story on the map in the iPad app should cause the timeline visible alongside the map to scroll to the selected story so you can see both the time and place that relates to the story. However I knew the timeline wouldn't be visible alongside the map on the iPhone. As such I needed to develop the map as a distinct self-contained thing separate from the timeline.

After thinking it through, I determined the app would consist of:

  • A map view controller
  • A timeline view controller
  • A story detail view controller
  • And two container view controllers for the iPhone and iPad versions

The map view controller would be responsible for displaying the list of stories it was given on a map.

The timeline view controller would be responsible for displaying the list of stories it was given on a timeline.

In order to synchronise the map and timeline views in the iPad version I also needed some way to highlight a story on the map or in the timeline when the same story was selected in the other view. However, as explained previously I didn't want to tightly couple the timeline to the map as I knew the two wouldn't appear alongside each other in the iPhone version of the app.

Highlighting a given story was the obvious bit. Both my map and timeline view controllers would need methods to highlight a given story:

// IndexMapViewController.h


@interface IndexMapViewController : UIViewController

/**
* Scrolls the map to the given story and displays its annotation.
*/
- (void)selectStory:(Story *)story;

@end


@interface StoryTimelineViewController : UICollectionViewController

/**
* Scrolls the timeline to the given story and changes the
* style of the story's cell to indicate it is selected.
* Pass nil to clear the selection.
*/
- (void)selectStory:(Story *)story;

@end

Slightly less obvious was how to ensure the timeline's setStory: is called when a story is selected in the map view and vice versa. This is where delegation comes in. I know that I want the map view to inform some other class when a story is selected (though at this stage of the design of the app I wasn't entirely clear which class that'd be) so using a delegate is ideal. It let's me clearly define the different responsibilities. It's a way of saying, if you're interested in knowing when a story is selected, register your interest with me and I'll tell you. In this way, the map view controller's responsibilities are expanded beyond simply showing the list of stories to also include informing an interested delegate when a story is selected. It's not the map view controller's responsibility to do anything about the fact a story was selected, it simply has to inform its delegate.

// IndexMapViewController.h


@protocol IndexMapViewControllerDelegate <NSObject>

- (void)indexMapViewController:(IndexMapViewController *)controller didSelectStory:(Story *)story;

@end

Likewise, the timeline view controller has a similar delegate protocol:

// StoryTimelineViewController.h


@protocol StoryTimelineViewControllerDelegate <NSObject>

- (void)storyTimelineDidSelectStory:(Story *)story;

@end

The final piece of the puzzle is finding a class to take responsibility for acting as the map and timeline view controllers delegate to coordinate the two.

// IndexViewController.h

@interface IndexViewController : UIViewController <IndexMapViewControllerDelegate, StoryTimelineViewControllerDelegate>
@end

Now the division of responsibilities is clear. The map and timeline view controllers take responsibility for displaying the list of stories they've been given and informing their delegate when a story has been selected. The index view controller is responsible for providing both with the list of stories to display and acts as the delegate for both, ensuring that when a story is selected in one it is also selected in the other. But how does all this look in the Storyboard? How does the index view controller obtain references to the map and timeline view controllers in order to pass them their list of stories and register as their delegate? It's all done through the magic of view controller containment.

A section of the storyboard showing an index view controller with container view controllers for the map and timeline view controllers.

In the storyboard, the index view controller's view has two Container View Controllers embedded within it. One for the map and one for the timeline.

However, whilst this provides an easy way to create the parent-child relationship between the index view controller and its two children, it doesn't actually create a reference between the classes. To actually obtain these references and pass messages and data between the view controllers you have to write a bit of code:

// IndexViewController.h

@interface IndexViewController : UIViewController <IndexMapViewControllerDelegate, StoryTimelineViewControllerDelegate>

@property (nonatomic, strong) IndexMapViewController *mapViewController;
@property (nonatomic, strong) StoryTimelineViewController *timelineViewController;

@end
// IndexViewController.m

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
// if this is an embed segue then keep a reference to the view controller
// that is being embeded
if ([segue.identifier isEqualToString:@"EmbedMap"]) {
IndexMapViewController *mapViewController = (IndexMapViewController *)segue.destinationViewController;
[self setMapViewController:mapViewController];
[self.mapViewController setDelegate:self];
}
if ([segue.identifier isEqualToString:@"EmbedTimeline"]) {
StoryTimelineViewController *timelineViewController = (StoryTimelineViewController *)segue.destinationViewController;
[self setTimelineViewController:timelineViewController];
[self.timelineViewController setDelegate:self];
}
}

The prepareForSegue: method is called when the view controller should prepare for a certain segue. When using segues this is your view controller's opportunity to do something before the app segues to a new view controller. Typically, this method is used to pass variables to view controllers you're transitioning to. However, it can also be used to obtain a reference to any child view controllers that you've added using container view controllers in your storyboard. The code here simply looks for the segue identifiers for the 'embed segues' used to embed the map and timeline view controllers. It then casts each segue's 'destinationViewController' to the type I know it must be before storing a reference to it and setting the IndexViewController as the delegate.

The final step is to implement the map and timeline view controller's delegate methods:

// IndexViewController.m

- (void)indexMapViewController:(IndexMapViewController *)controller didSelectStory:(Story *)story
{
[self.timelineViewController selectStory:story];
}

- (void)storyTimelineDidSelectStory:(Story *)story
{
[self.mapViewController selectStory:story];
}

This might seem a little more complex than simply allowing the map view controller to know about the timeline view controller and vice versa, but this approach offers the flexibility to use these components independently of one another. Creating the iPhone version of the app was straightforward as a result.

The iPhone app uses the same map view controller, but   this time it is contained within its own tab.