Lessons From iOS: UISplitViewController

UISplitViewController is one of the fundamental view controllers for the iPad, but using it for the first time can present a challenge. A good example of a split view controller is the mail application. The sidebar(or popover in portrait) on the left lists emails or mailboxes. the main panel on the right shows the content of an email. There are a number of moving pieces that need to fit together. Typically the UISplitViewController contains two UINavigationViewControllers. The view controllers are referenced are UINavigationViewControllers.  The first navigation view controller is called the master view.  The master view usually has a table view controller as its root view controller and appears as sidebar in landscape and a popover in portrait. The second view controller is referred to as the detail view controller. The detail view is typically visible either as a main panel in landscape or as full screen view in portrait.

So usually there is one UISplitViewController, two UINavigationViewControllers, one UITableViewController, and one custom view controller.
To avoid ambiguity I will refer to them as the split controller, detail nav, master nav, master controller, and detail controller to refer respectively to each of these views.

The general preference of the Cocoa frameworks is to delegate rather than to subclass. Chance are there is nothing special about either of your nav views or the master controller so these can be delegated to another object. The same could be done with the UISplitViewController, but if you keep delegating everything further back then your either going to end up with an application delegate that is monstrous, or you have view controllers that are acting as delegates for views they are unrelated to, or you create an object to act as delegate for a collection of things. Since the split controller is all about holding other views it made sense to to subclass UISplitViewController and make the subclass act as the delegate for the views and controllers it contains.

Using this design the split controller adopts the UINavigationController delegate to support the detail nav (master nav is usually static). It will need to support UITableViewDelegate and UITableViewDataSource to support selecting elements from the master controller. It will also need to support UISplitViewController and UIPopoverController delegates(The first to support orientation changes and the latter to present the master view in portrait orientation). If your detail view has a custom delegate, the split controller most also adopt it.

A very natural use of UISplitViewController is to list a collection of items briefly in the master controller and display the object at length in the detail view (Like the Mail or notes app). Another design pattern is to use it as an alternative graphical representation of a tab bar. Instead of listing the tabs along the bottom, the tabs can be listed along the side inside the master controller. In this case the detail controller is not one controller, but one of a collection of controllers.  

There are a number of moving peaces that all need to work together to produce a functioning split controller. The first step is to tie everything together. Somewhere outside the split controller class it needs to have its splitView delegate and its popover delegate set to itself. After that is handled all the rest of the heavy lifting is done inside the split controller class. During my experimentation I found that in order to prevent strange bugs it was necessary to retain the popover controller and the button that presents the popover view.  

@interface LETUSplitController ()

@property (nonatomic, readwrite) UINavigationController * detailNav;
@property (nonatomic, strong) UIBarButtonItem *splitViewButton;
@property UIPopoverController * splitPopover;

@end

There are four key methods to handle the split view behavior. The first is setting delegates and settings. This is easily done inside the viewDidLoad method.

-(void)viewDidLoad
{
  //set the master nav delegates and title
  UITableViewController * masterView = (id)[self.viewControllers[0] topViewController];
  masterView.title = @"myTitle";
  masterView.tableView.delegate = self;
  masterView.tableView.dataSource = self;

  //Set the detail nav delegate
  self.detailNav = self.viewControllers[1];
  self.detailNav.delegate = self;
  
  //set a couple of settings
  masterView.clearsSelectionOnViewWillAppear = NO;
  masterView.contentSizeForViewInPopover = CGSizeMake(320.0, 600.0);
  
  //do your extra stuff here
}

The next step is to configure the splitViewController delegate methods. These methods are responsible for handling the orientation changes and presenting the popover and the button.  

- (void)splitViewController:(UISplitViewController*)splitController 
willHideViewController:(UIViewController *)viewController 
withBarButtonItem:(UIBarButtonItem *)barButtonItem
forPopoverController:(UIPopoverController *)popoverController
{
  barButtonItem.title = @"myTitle";
  [[[self.viewControllers[1] topViewController] navigationItem] setLeftBarButtonItem:barButtonItem animated:YES];
  self.splitViewButton = barButtonItem;
  self.splitPopover = popoverController;
}

- (void)splitViewController:(UISplitViewController *)splitController
     willShowViewController:(UIViewController *)viewController
  invalidatingBarButtonItem:(UIBarButtonItem *)barButtonItem
{
  [[[self.viewControllers[1] topViewController] navigationItem] setLeftBarButtonItem:nil animated:YES];
  self.splitViewButton = nil;
  self.splitPopover = nil;
}

The next step is to respond to the user selecting an item from the the mater view. For whatever reason the button likes to disappear after a selection so I just re-inserted it after each selection.

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
  UITableViewCell * cell = [tableView cellForRowAtIndexPath:indexPath];
  
  //use the cell to decide what to do. l\Load a new detail view or update the current one
  
  [self.splitPopover dismissPopoverAnimated:YES];
  [[[self.viewControllers[1] topViewController] navigationItem] setLeftBarButtonItem:self.splitViewButton animated:YES];
}

The last step is to handle all of he data source methods for the master view. This is just standard TableViewController stuff.  I had a deuce of a time getting all the moving pieces to work together. What I present here should be the bear bones essentials required to get it all to work. If you have troubles drop me a comment and maybe I can help you. Also let me know if you have questions or problems.