Adventures in UISplitViewController

I love sample code. Its a reassuring proof of the documentation and make it really easy to copy and paste. With fewer keystrokes I make fewer mistakes. So as a payment to the karma of all the code that I’ve pored over Im contributing sample code as well.

Recently, I finished an application that used a UITabViewController to manage multiple master controllers. The starting point was MultipleDetailViews. Its a great starting point but doesn’t leverage the new Storyboard paradigm. I’m going to re-implement this sample to iOS 5.

The UISplitViewController manages two view controllers, a master controller and a detail controller. The master controller manages the left portion in landscape mode and the popover while in portrait mode. The detail view controller is always visible.

This demo extends that by using a Tab view controller to manage multiple master controllers and swapping in detail controller associated with the active master controller.


Step 1: Create the Project

Open up Xcode and from the main menu choose File\New\New Project. Choose the iOS\Application\Master-Detail Application template, and click Next. Name the product MultipleMasterDetailViews, select iPad for the Device family, and make sure just the Use Automatic Reference Counting checkbox is checked, click Next and save the project by clicking Create.

Step 2: Modify the Storyboard

The UISplitViewController contains UINavigationController contains MDMasterViewController. This project inserts a UITabBarController as a container between the UISplitViewContoller and the UINavigationController. Do this by dragging a UITabBarController onto the storyboard and option drag from the UISplitViewController to the UITabBarController. Select Relationship-masterViewController when prompted. Delete the default UIViewControllers from the UITabBarController and add the UINavigationController/MDMasterViewController as a tab. Duplicate the UINavigationController/MDMasterViewController and this second one as a tab as well. We’re reusing the MDMasterViewController for demonstration purposes only. In practice they might be different classes.

The final modification to the storyboard is to add another scene for the second detail view. Using the current UINavigationController/MDDetailViewController as a prototype, copy and paste a new scene into the Storyboard. This new scene needs an identifier because it is loaded using instantiateViewControllerWithIdentifier:(NSString*).

Set the identifier property of the UINavigationController to “Detail 2 Root” (I wish that Xcode would automatically create a header file to automate this fragile mapping, hint, hint).

The Storyboard layout should now be similar to this.

Step 3: Master Detail Manager

The Apple sample code implements the UISplitViewControllerDelegate in the detail view controller. I’m using the Composition pattern to implement this functionality for increased reusability. Create a new class that implements the UISplitViewControllerDelegate and  UITabBarDelegate. This implementation assumes that all the DetailViewControllers are imbedded in a UINavigationController and that the first item  of the UISplitVeiwController.viewControllers is a UITabBarController. This initWithSplitViewController: updates the delegate properties of both the splitViewController and the master controller (the first controller) to self. The array that’s provided must also have the same number of detail controllers as controllers in the tabBarController.viewControllers.

@interface MDMultipleMasterDetailManager
           : NSObject

-(id)initWithSplitViewController:(UISplitViewController*)splitViewController
       withDetailRootControllers:(NSArray*)detailControllers;

@end

The implementation of initWithSplitController is relatively straight forward.

@implementation MDMultipleMasterDetailManager

@synthesize splitViewController = __splitViewController;
@synthesize detailControllers = __detailControllers;
@synthesize masterBarButtonItem = __masterBarButtonItem;
@synthesize masterPopoverController = __masterPopoverController;
@synthesize currentDetailController = __currentDetailController;

-(id)initWithSplitViewController:(UISplitViewController*)splitViewController withDetailRootControllers:(NSArray*)detailControllers
{
    self = [super init];
    if(self){
        __splitViewController = splitViewController;
        __detailControllers = [detailControllers copy];
        UINavigationController* detailRoot = [splitViewController.viewControllers objectAtIndex:1];
        __currentDetailController = detailRoot.topViewController;

        splitViewController.delegate = self;
        UITabBarController* tabBar = [splitViewController.viewControllers objectAtIndex:0];
        tabBar.delegate = self;
    }

    return self;
}

The UISplitViewControllerDelagate methods have to manage the currently active detail controller and maintain enough state to update other detail controllers if they become active. This implementation modifies the left button of the detail controllers directly.

/* forward the message to the current detail view
 */
-(void)splitViewController:(UISplitViewController *)svc willHideViewController:(UIViewController *)aViewController withBarButtonItem:(UIBarButtonItem *)barButtonItem forPopoverController:(UIPopoverController *)pc
{
    self.masterBarButtonItem = barButtonItem;
    self.masterPopoverController = pc;

    barButtonItem.title = NSLocalizedString(@"Master", @"Master");

    [self.currentDetailController.navigationItem setLeftBarButtonItem:self.masterBarButtonItem animated:YES];
}

/* forward the message to the current detail view
 * all detail views must implement UISplitViewControllerDelegate
 */
-(void)splitViewController:(UISplitViewController *)svc willShowViewController:(UIViewController *)aViewController invalidatingBarButtonItem:(UIBarButtonItem *)barButtonItem
{
    self.masterBarButtonItem = nil;
    self.masterPopoverController = nil;

    [self.currentDetailController.navigationItem setLeftBarButtonItem:nil animated:YES];

}

The UITabBarControllerDelegate just swaps the button and popover from the previous detail controller to the detail controller associated with the currently selected master controller.
According the the UIPopoverController documentation the navigation bar is automatically included in the passthrough views. As the detail controllers are exchanged, the passthrough views of the popover need to be updated as well.

// change detail view to reflect the current master controller
-(void)tabBarController:(UITabBarController *)tabBarController didSelectViewController:(UIViewController *)viewController{
    UINavigationController* detailRootController = [self.detailControllers objectAtIndex:tabBarController.selectedIndex];
    UIViewController* detailControler = detailRootController.topViewController;

    if(detailControler != self.currentDetailController)
    {
        [self.currentDetailController.navigationItem setLeftBarButtonItem:nil animated:NO];
        self.currentDetailController = detailControler;

        UIViewController* tabBarController = [self.splitViewController.viewControllers objectAtIndex:0];

        self.splitViewController.viewControllers = [NSArray arrayWithObjects:tabBarController,detailRootController, nil];

        // replace the passthrough views with current detail navigationbar
        if([self.masterPopoverController isPopoverVisible]){
            self.masterPopoverController.passthroughViews = [NSArray arrayWithObject:detailRootController.navigationBar];
        }
    }
}

Finally, the application delegate needs to be modified to wire this all up. Because we’ve used the delegate pattern, it’s only a couple of lines of code in didFinishLaunchingWithOptions.

@interface MDAppDelegate()
@property (strong,nonatomic)MDMultipleMasterDetailManager* masterDetailManager;
@end

@implementation MDAppDelegate

@synthesize window = _window;
@synthesize masterDetailManager = __masterDetailManager;

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    UISplitViewController *splitViewController = (UISplitViewController *)self.window.rootViewController;

    UIViewController* detail1 = [splitViewController.viewControllers objectAtIndex:1];
    UIViewController* detail2 = [splitViewController.storyboard instantiateViewControllerWithIdentifier:@"Detail 2 Root"];

    self.masterDetailManager = [[MDMultipleMasterDetailManager alloc] initWithSplitViewController:splitViewController
                                withDetailRootControllers:[NSArray arrayWithObjects:detail1,detail2,nil]];

    return YES;
}

And the Link to the GIT Repository

Here’s a link to public GIT repository project if you want to look at a working example. I hope you find it useful.

This entry was posted in iDevBlogADay and tagged . Bookmark the permalink.

5 Responses to Adventures in UISplitViewController

  1. Michael says:

    Nice read! but none of your images are showing :(

  2. Pingback: Tutorial: iPad UISplitViewController And Storyboards | iPhone, iOS 5, iPad SDK Development Tutorial and Programming Tips

  3. Pingback: Tutorial: iPad UISplitViewController And Storyboards |