Multiple Views with a UISplitViewController

The iPad has ushered in a suite of new awesome user interface paradigms with it’s large screen and enhanced performance.
In this article, I’ll step through using one of the new user interface
elements in iPhone SDK 3.2+, the UISplitViewController, in particular
managing multiple views with their own navigation controller stack,
handling all orientations.
Concept
Since I’m an avid cyclist and the Giro d'Italia just finished, in this article’s we’ll step through an example application that lists the name and distance of each stage.
When tapped, we’ll show a photo from Melbourne’s most popular cycling blog, Cycling Tips, and allow the user to tap through to the Cycling Tips article summarizing the stage (full credits to Cycling Tips, they’ve done an awesome job at covering the Giro this year).
I’ll use landscape orientation terminology to describe the visual aspects of the split view controller in this article, but please assume normal split view controller semantics with the left hand side of the split view appearing in a popup view when in portrait mode. We’ll discuss the code required to get this to work as well.
Model
Since we want to focus on the UISplitViewController, I’ll browse
over the data model for this particular application since it is quite
small, all the code is
available online, and
it’s just a single Core Data Stage entity containing attributes
relevant to each stage.
I definitely recommend utilizing Core Data for your models where possible, I find it fast and easy to use, and its great for prototyping new ideas.
For data persistence it’s awesome, you can switch between using in-memory storage and SQLlite during the development making it easy to always have fresh content while you’re making changes.
Table view list of Stages
On to the controller code, along the left hand side of the split view controller, we’ll show a table view, with each stage of the Giro being listed in a separate cell.
StageTableViewController Interface
@interface StageTableViewController : UITableViewController { NSFetchedResultsController * stageResultsController; NSMutableDictionary * stageViewControllers; UIPopoverController * popoverController; UIBarButtonItem * popoverButtonItem; } @property (nonatomic, retain) NSFetchedResultsController * stageResultsController; @property (nonatomic, retain) NSMutableDictionary * stageViewControllers; @property (nonatomic, retain) UIPopoverController * popoverController; @property (nonatomic, retain) UIBarButtonItem * popoverButtonItem; - (void)autoselectFirstStage; @end @protocol PopupManagingViewController - (void)showPopoverButtonItem:(UIBarButtonItem *)barButtonItem; - (void)invalidatePopoverButtonItem:(UIBarButtonItem *)barButtonItem; @end
The stageResultsController property provides access to our Core Data backed
model.
In addition to this we define storage for the known view controllers we present on the right hand side of the split view controller. This effectively caches view controllers we’ve already shown, so if the user taps back to them, they show instantly rather than re-fetch data.
We also define an autoselectFirstStage method that the application
delegate can use to automatically select and show the first stage upon
startup.
When orientated to portrait, the left hand side of the split view controller
disappears and becomes available via a popup. The popoverController
property is responsible for showing the view controller inside a popover
view when a user taps the popoverButtonItem button.
We also define a protocol our detail view controller can implement to install and remove the button allowing access to the popover when we switch between detail views in portrait mode.
StageTableViewController Implementation
@implementation StageTableViewController @synthesize stageResultsController, stageViewControllers, popoverButtonItem, popoverController; - (void)viewDidLoad { [super viewDidLoad]; self.title = @"Stages"; self.stageResultsController = [[ContentController sharedInstance] stageResultsController]; NSUInteger stageCount = [self tableView:self.tableView numberOfRowsInSection:0]; self.contentSizeForViewInPopover = CGSizeMake(320.0, self.tableView.rowHeight * stageCount); self.clearsSelectionOnViewWillAppear = NO; self.stageViewControllers = [NSMutableDictionary dictionary]; } - (void)autoselectFirstStage { NSIndexPath * startPath = [NSIndexPath indexPathForRow:0 inSection:0]; [self.tableView selectRowAtIndexPath:startPath animated:NO scrollPosition:UITableViewScrollPositionNone]; [self.tableView.delegate tableView:self.tableView didSelectRowAtIndexPath:startPath]; }
In viewDidLoad, we configure the view controller and create
references to our model layer’s NSFetchedResultsController. Our
autoselectFirstStage method ensures the table view and delegate are
informed of our selection.
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return [[self.stageResultsController sections] count];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
id <NSFetchedResultsSectionInfo> sectionInfo = [[self.stageResultsController sections] objectAtIndex:section];
return [sectionInfo numberOfObjects];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = @"StageCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier] autorelease];
}
Stage * stage = [self.stageResultsController objectAtIndexPath:indexPath];
cell.textLabel.text = stage.name;
cell.detailTextLabel.text = stage.formattedDistance;
return cell;
}
We then implement the required UITableViewDataSource protocol methods
to return our content to fill the table view when requested.
For some extra shine, we would also want to implement a custom
UITableViewCell as well to nicely show all the stage content. I’ll
follow this up in a subsequent article.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { Stage * stage = [self.stageResultsController objectAtIndexPath:indexPath]; // invalidate the current popover button if one is being used UINavigationController * currentStageNavigationController = self.splitViewController.rightSideViewController; UIViewController<PopupManagingViewController> * currentViewController = [currentStageNavigationController rootViewController]; [currentViewController invalidatePopoverButtonItem:self.popoverButtonItem]; // install the new viewcontrollers array (LHS: stage view controller, RHS: incoming detail navigation controller) UINavigationController * stageNavigationController = [self.stageViewControllers valueForKey:stage.name]; if (!stageNavigationController) { StageDetailViewController * detailViewController = [[StageDetailViewController alloc] initWithStage:stage]; stageNavigationController = [[UINavigationController alloc] initWithRootViewController:detailViewController]; [detailViewController release]; [self.stageViewControllers setValue:stageNavigationController forKey:stage.name]; [stageNavigationController release]; } self.splitViewController.viewControllers = [NSArray arrayWithObjects:self.navigationController, stageNavigationController, nil]; // dismiss the popover if present, and add the popover button to the incoming detail view controller [self.popoverController dismissPopoverAnimated:YES]; if (self.popoverButtonItem) { UIViewController<PopupManagingViewController> * viewController = [stageNavigationController rootViewController]; [viewController showPopoverButtonItem:self.popoverButtonItem]; } }
The only UITableViewDelegate method we’ll implement is
tableView:didSelectRowAtIndexPath: for when a user taps a particular
stage in the table view.
In tableView:didSelectRowAtIndexPath: we perform a few
tasks. Essentially, we want to create a StageDetailViewController
instance within a navigation controller, and cache it in case the user
selects this table row again.
Once we’ve created (or retrieved a cached version of) our
StageDetailViewController we reassign the viewControllers property
on the split controller, specifying the new left/right hand side view
controllers to be shown.
In this case, we never want the left hand side to change from being
the table view of all stages, so position 0 in the returned array
is the navigationController reference of the StageTableViewController
(be careful to remember this in your apps as you’ll get some funny
behaviour if you return the view controller directly).
Position 1 of the array includes the newly created stage detail view
controller, inset within a UINavigationController instance.
Once the ‘viewController’ property has been assigned, the split view updates instantly to show our selected view controller in the right hand side of the split view.
In addition to this, if the user selected an item in the table while in portrait mode from a popover, we dismiss the popover view, and install a new popover button in the detail view that the user can tap if they wish to select another item.
Finally as good memory management citizens we also implement
viewDidUnload and didReceiveMemoryWarning to release allocated
and cached resources respectively.
- (void)viewDidUnload { [super viewDidUnload]; self.stageViewControllers = nil; self.popoverButtonItem = nil; self.popoverController = nil; self.stageResultsController = nil; } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; [self.stageViewControllers removeAllObjects]; }
The full XCode project
also includes the UISplitViewController delegate code to
install/remove the popover button when the user changes orientation.
Now that we’ve implemented the left hand side of the split view, let’s move onto the right hand side detail view.
StageDetailViewController interface
The right hand side of the split view controller will initially show a picture from the selected Giro racing stage. We’ll also place this image inside a scroll view so the user can pan and zoom in/out, etc.
We will also install a button within the navigation bar so the user can drill down to the blog post summarizing that stage.
Here’s the interface:
@interface StageDetailViewController : UIViewController <ImageLoaderDelegate, UIScrollViewDelegate, PopupManagingViewController> { Stage * stage; UIScrollView * scrollView; UIActivityIndicatorView * activityView; UIImageView * stageImageView; ImageLoader * imageLoader; } @property (nonatomic, retain) Stage * stage; @property (nonatomic, retain) IBOutlet UIScrollView * scrollView; @property (nonatomic, retain) IBOutlet UIActivityIndicatorView * activityView; @property (nonatomic, retain) IBOutlet UIImageView * stageImageView; @property (nonatomic, retain) ImageLoader * imageLoader; - (id)initWithStage:(Stage *)stage; @end
The ImageLoader property is a helper class I’ve written for asynchronously
downloading images over the network, utilizing the excellent
ASIHttpRequest library.
StageDetailViewController implementation
@interface StageDetailViewController () - (void)didSelectReadArticle:(id)sender; @end @implementation StageDetailViewController @synthesize stage, stageImageView, imageLoader, activityView, scrollView; - (id)initWithStage:(Stage *)aStage { if (self = [super initWithNibName:@"StageDetailViewController" bundle:nil]) { self.stage = aStage; } return self; } - (void)viewDidLoad { [super viewDidLoad]; self.title = [NSString stringWithFormat:@"Giro d'Italia: %@", self.stage.name]; self.view.backgroundColor = [UIColor scrollViewTexturedBackgroundColor]; self.scrollView.maximumZoomScale = 5.0f; self.scrollView.minimumZoomScale = 1.0f; self.scrollView.delegate = self; self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Read Article" style:UIBarButtonItemStylePlain target:self action:@selector(didSelectReadArticle:)]; self.navigationItem.backBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:[NSString stringWithFormat:@"Stage %@", self.stage.index] style:UIBarButtonItemStylePlain target:nil action:nil]; self.imageLoader = [[ImageLoader alloc] initWithURL:self.stage.imageURL]; self.imageLoader.delegate = self; [self.imageLoader start]; } - (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView { return self.stageImageView; }
In the anonymous category we define a didSelectReadArticle: method
to be invoked when the taps the button to drill down to read the full
blog post summarizing the race stage.
In viewDidLoad, we configure the view’s title and background colour
(scrollViewTexturedBackgroundColor looks just awesome on the iPad),
the scroll view’s scale and delegate properties, install a ‘Read
Article’ button on the right hand side of the navigation bar, and
redefine the back button to be the stage number rather than the full
stage name.
Finally, we create an ImageLoader instance and start retrieving an
image of the specified stage.
Should a user tap the ‘Read Article’ button the following method is called:
- (void)didSelectReadArticle:(id)sender { StageArticleViewController * articleViewController = [[StageArticleViewController alloc] initWithStage:self.stage]; [self.navigationController pushViewController:articleViewController animated:YES]; [articleViewController release]; }
This will push an instance of StageArticleViewController onto the
navigation stack, which includes a web view configured to load the
race stage summary.
Once again we ensure all allocated resources are released when the view is deconstructed.
- (void)viewDidUnload { [super viewDidUnload]; self.stageImageView = nil; self.imageLoader.delegate = nil; self.imageLoader = nil; self.scrollView = nil; } - (void)dealloc { self.stage = nil; [super dealloc]; }

Here we split deallocation of the view related items from the stage,
since the stage was specified in the constructor whereas
stageImageView, scrollView and the imageLoader objects were
created in viewDidLoad.
Summary
As we can see, UISplitViewController is really useful for
master-detail style interfaces and it’s configured similarly to a
UITabBarController by assigning it’s viewControllers property.
I’ve skipped over a few parts of the example app to keep the post
length under control, such as implementation of the
StageArticleViewController, orientation permissions on all our view
controllers, the image loader callbacks to add the image to the scroll
view upon completion of download, etc. All of this is availablae in
the full XCode project
however, please feel free to peruse the code online.

