Friday, April 16, 2010

Retrofitting iPad UISplitViewControllers

Here's a technique I developed while adding iPad support to our existing iPhone app. The view hierarchy is defined in a bog-standard iPhone XIB file, with a UITabBarController at its root. Each tab bar has a UINavigationController, which in turn holds a UIViewController subclass.

Creating a new XIB for the iPad and adding UISplitViewControllers to each tab was too much for my lazy bones, so I decided to do it programmatically at runtime, in my app delegate. Here's what the code looks like:
// Masterful retrofit hack: Wrap the view controllers inside split view controllers
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
  NSMutableArray *controllers = [NSMutableArray arrayWithCapacity:[tabBarController.viewControllers count]];

  for (UIViewController *controller in tabBarController.viewControllers) {
    UISplitViewController *split = [[[UISplitViewController alloc] init] autorelease];
split.tabBarItem = controller.tabBarItem;
    DetailViewController *detail = [[[DetailViewController alloc] init] autorelease]; // a detail view will come here
    UINavigationController *nav = [[[UINavigationController alloc] initWithRootViewController:detail] autorelease];
    split.viewControllers = [NSArray arrayWithObjects:controller, nav, nil];
    [controllers addObject:split];

  tabBarController.viewControllers = controllers;
The code is iterating over the tab bar's controllers and inserting a split view into the mix.

At the other point in our code (where we would normally push views onto the navigation controllers), we detect the presence of a split view and do the appropriate thing:
if ([self.navigationController.parentViewController isKindOfClass:[UISplitViewController class]]) {
// iPad split view controller support
UISplitViewController *split = (UISplitViewController *)self.navigationController.parentViewController;
  UINavigationController *nav = [split.viewControllers objectAtIndex:1];
  DetailViewController *detail = [nav.viewControllers objectAtIndex:0];
  [detail initWithModel:phoneBookModel entryAtIndex:indexPath.row];
} else {
  UIViewController *controller = [[DetailViewController alloc] initWithModel:phoneBookModel entryAtIndex:indexPath.row];
  [self.navigationController pushViewController:controller animated:YES];
  [controller release];


G said...

i tried your hack . but i m not getting split view format .
Do you have any example which i run in the xcode.

please help in m stuck

ganeshfaterpekar at gmail dot com

Joe said...

Hi Vikram,

If you were to attempt the same sort of thing using a XIB instead of doing it programmatically, can you describe your approach? I've not found anything else that describes how to do this. I'm not clear why Apple is discouraging this behavior as it only seems natural. In my case, I have a splash screen that uses a horizontal UIScrollView to display the first level of navigation and I'm looking to use the UISplitViewController as the second level. Since I'm writing this from scratch, I'd like to use XIB's as much as possible, but I'm certainly open to using code if that's the only way to accomplish this.

Thanks in advance!


Anonymous said...

SO when doing this and launching in Portrait Mode, the Navigation BarButtonItem does not appear so that the Master Detail View can be displayed in a popover. Any suggestions on how to do this. I am having a difficult time getting all orientations to work at startup.

hac76610 said...

Bob, you have just to set detailViewController as delegate of UISplitViewController

Jassi said...

How to implement SplitViewController on second level.

Actually what i want is to launch app with a login page and after login. I need SplitViewController.

Please help...

Greg Combs said...
This comment has been removed by the author.
Greg Combs said...

I've written a subclass of the UISplitViewController that's smarter about device orientation changes when it's not the frontmost tab in a UITabBarController. See the repository on GitHub