Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Mobile / iOS

Versatile, programmer-friendly Split View Controller for iOS

4.71/5 (14 votes)
20 Jan 2013CPOL10 min read 91.6K   1.8K  
An Objective-C class that sub-classes UIViewController to enable easily implementing split views in iPad applications.

Introduction  

This project implements a split view controller for iPad. It is a lot like the UISplitViewController class in the iOS library, but it is more versatile and easy to use. In this article, we will refer to the split view controller being developed as simply the split view controller and the one in the iOS library will be referred to as UISplitViewController. The split view controller is compatible with iOS 4.2 and up. 

The split view controller displays two view controllers next to each other: a side view controller on the left side of the screen, and a main view controller displayed slightly larger on the right side. It supports all the user interface orientations that are supported by these two view controllers. Throughout this article, we will refer to the side view controller and the main view controller collectively as the sub-view controllers.

Here are some of the differences between this split view controller and the UISplitViewController that is part of the iOS library:

  • The split view controller has a flag that allows the client to choose if the side view controller must be hidden when iPad is in portrait orientation. UISplitViewController always hides the side view controller in this orientation.
  • Unlike UISplitViewController, the split view controller does not have to be the root controller of the app. A UISplitViewController cannot be pushed onto a UINavigationController's stack and cannot be presented modally. The split view controller does not impose these restrictions.
  • The split view controller's list button can be displayed in a tool bar or a navigation controller's navigation bar. We call it the list button since it displays the side view controller's view in a popover view controller, and the side view controller is typically a UITableViewController that shows a list of items (this need not be the case; the side view controller can be an arbitrary UIViewController object).
  • The split view controller can easily be set up programmatically. In fact, that is how we use it in the example project in the source code
  • The split view controller's view splitter need not be a 1 pixel wide black line. It can be an arbitrary UIView object.
  • The split view controller has a property that controls if the side view is displayed on the left side or on the right side of the main view.

The screenshot below shows what the split view controller looks like in a UINavigationController in a landscape orientation in the example project:

splitviewcontroller/photo-2.png

Here is what it looks like in portrait orientation, after pressing the list button, using the popover style: 

splitviewcontroller/photo-1.png

Here is what it looks like using the slide-over style:

Image 3

Implementation 

The split view controller is implemented by the class SplitViewController, which is a sub-class of the UIViewController class. SplitViewController initializes from a XIB file, via the initWithNibName:bundle: method. The standard XIB file, SplitView.xib, which is shown in the image below, contains a root UIView, with a single sub-view. We call this sub-view the view splitter. In this XIB file, it is a black vertical line (specifically, it's a plain UIView with a black background, a width of 1, and a height equal to the height of its parent view) whose purpose is to separate the views of the side view controller and the main view controller. 

splitviewcontroller/SplitView.xib.png

The x-location of the view splitter defines the (vertical) split point of the sub-view controllers, although the client of the split view controller can override this split point through the splitPoint property (this property sets the x-coordinate of the center of the view splitter). For aesthetic motivations, the x-location of the view splitter in SplitView.xib has been chosen so that the golden ratio applies when the split view controller occupies all of iPad's screen in landscape orientation (1004/620 = 1.61935...; 620/384 = 1.61458...). The split view controller basically works as follows: when the side view controller and the main view controller are assigned to the split view controller, the split view controller adds the views of those view controllers as sub-views of its main-view, using the split view's frame to properly set the locations and dimensions of the view controller's views. When the interface orientation is rotated, the frames of the views of the sub-view controllers are adjusted accordingly with a nice animation.  

The client chooses whether the side view controller should be hidden in portrait orientation by the setting the value of the sideViewDisplayOption property (whose default value is SideViewDisplayOptionAlwaysDisplay). If this property is set to SideViewDisplayOptionHideInPortraitOrientation and the orientation changes from landscape to portrait, the side view controller's view is removed from the split view controller's view and a list button is added to the left of the navigation controller's navigation bar (if there is one). The text used for the list button is the value of the listButtonTitle property.  

There is a problem that needs to be dealt with here when iOS 4.2 or iOS 4.3 has to be supported: in these versions of iOS, UINavigationItem can only have one left button. That implies that we cannot add the list button if there already is a back button or some other left-hand-side button. The workaround that SplitViewController makes provision for is to display a single button on the left that looks like two buttons. This is a bit of a hack'esque solution, but it works. The button is a UIBarButtonItem with a custom view that displays a UIImage. When the button is pressed, we determine which of the two buttons were really pressed by inspecting the location of the touch. In order to have a UITouch object at hand to get a location from, we implement the class MyImageView, which is a sub-class of UIImageView. This class simply overrides the touchesEnded:withEvent: method of UIView, where it calls a selector of the SplitViewController with the set of UITouch objects as a parameter.

We have described how SplitViewController works when it is one of the view controllers in the stack of a UINavigationViewController. This is not the only way SplitViewController can be used. It can be used without a bar at the top. This makes sense when the split view controller does not hide the side view (that is, if hideSideViewInPortraitOrientation is set to NO), but if it does hide the side view, the list button needs to be displayed somewhere. The alternative is to use SplitViewController with a UIToolbar. In this case, you will have to create a custom XIB file that contains a UIToolbar with a UIToolbarButtonItem that will be used for the list button. The file ToolbarSplitView.xib in the example code is an example of such a custom XIB file. You must link up the toolbar and toolbarListButton IBOutlets to the UIToolbar and the UIToolbarButtonItem in the XIB, respectively. The reference to the UIToolbar is necessary only when the listDisplayStyle property of the split view controller is set to ListDisplayStylePopover, so that, when the list button is pressed, it can compute the location of the arrow of the popover controller, so that the arrow appears directly beneath the list button. Of course, when the split view controller is in landscape orientation, the list button needs to be hidden. SplitViewController hides the list button by setting its width to 0.1. When the list button needs to become visible again, its width is reset to its original value.

Using the code 

The source files that need to be added to an Xcode project in order to use the SplitViewController are those in the SplitViewControllerTestProject/SplitViewController folder in the zip file (Download source code - 72.7 KB).   

The Xcode project in the zip file attempts to demonstrate all the ways in which SplitViewController can be used, which I briefly explain in the following paragraphs.

  • To make the SplitViewController the root controller of an app, one only needs to make a few small changes in the app's delegate class (the class that implements the UIApplicationDelegate protocol). We assign the SplitViewController to the rootViewController property of the UIWindow of the app. The SplitViewController is then retained by the UIWindow, so we can release it. 
  • We create and display the SplitViewController in the application:didFinishLaunchingWithOptions: method like this: 

    Objective-C
    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
    {
        self.window = [[[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]] autorelease];
        self.window.backgroundColor = [UIColor whiteColor];
        
        topViewController = [[SplitViewController alloc] init];
        UIViewController* mainViewController = // Code to allocate and init main view controller.
        topViewController.mainViewController = mainViewController;
        UIViewController* sideViewController = // Code to allocate and init side view controller.
        topViewController.sideViewController = sideViewController;
        [sideViewController release];
        [mainViewController release];
        // Temporary fix for the 20px shift after launching app:
        CGRect initialFrame = topViewController.view.frame;
        initialFrame.origin.y+=20;
        topViewController.view.frame = initialFrame;
        self.window.rootViewController = topViewController; 
        [topViewController release];
    
        [self.window makeKeyAndVisible];
        return YES;
    }    

    Note that when the device orientation changes, SplitViewController first asks the side- and main view controllers if it should auto-rotate to that new interface orientation. It will return YES for the auto-rotation only when both sub-view controllers return YES. Therefore, make sure that in the shouldAutorotateToInterfaceOrientation: method of both sub-view controllers, return YES for every interface orientation that you want to support. Since iOS 6.0, the new supportedInterfaceOrientations method is called instead.

    How you make the side view controller and main view controller talk to each other is up to you. It is easy to obtain a reference to SplitViewController in the side- or main view controller - just add a property of type SplitViewController* and with the name splitViewController as in:

    Objective-C
    @interface MySideViewController : UITableViewController
    {
        SplitViewController* splitViewController;
    }
    
    @property (nonatomic, assign) SplitViewController* splitViewController; 
    
    @end  

    The setMainViewController: or setSideViewController: method of SplitViewController will find this property and assign the SplitViewController to it.

  • Here is the code to push a SplitViewController (which hides its side view controller in portrait orientation) on a navigation controller's stack:
  • Objective-C
    SplitViewController* nextSplitViewController = [[SplitViewController alloc] init];
    nextSplitViewController.title = @"Split view controller's title text";
    nextSplitViewController.sideViewDisplayOption = 
        SideViewDisplayOptionHideInPortraitOrientation;
    nextSplitViewController.listButtonTitle = @"Items";
    nextSplitViewController.sideViewHiddenLeftButtonsImage = [UIImage imageNamed:@"splitter_items.png"];
    UIViewController* mainViewController = // Code to allocate and init main view controller.
    nextSplitViewController.mainViewController = mainViewController;
    UIViewController* sideViewController = // Code to allocate and init side view controller.
    nextSplitViewController.sideViewController = sideViewController;
    sideViewController.contentSizeForViewInPopover = CGSizeMake(320,320);
    [mainViewController release];
    [sideViewController release];
    
    [myTopViewController.navigationController pushViewController:nextSplitViewController animated:YES];
    [nextSplitViewController release];      

    The sideViewHiddenLeftButtonsImage property only needs to be set if the app will run on iOS 4.2 or iOS 4.3. It is the image that simulates two buttons when the side view controller is hidden and the list button needs to be displayed, like this image:

    splitviewcontroller/backsplitter_items.png

    To make the image, run the app on a device with iOS 5.0+ (or on the simulator with iOS 5.0+), make a screenshot, and cut out the buttons.

  • To have the list button displayed in a UIToolbar, we must create our own XIB to use with the SplitViewController. See the file ToolbarSplitView.xib in the example code for an example. We create an iPad XIB, add a split view, and add a UIToolbar containing a UIToolbarButton (we can add many other UIBarItems to the UIToolbar if desirable). Set the class of the File's Owner to SplitViewController and connect the viewSplitter, toolbar, and toolbarListButton IBOutlets. If we want to bind the actions of any UIToolbarButton in the UIToolbar, other than the list button, we will have to do this programmatically, as in the example below.
  • Objective-C
    SplitViewController* nextSplitViewController = 
        [[SplitViewController alloc] initWithNibName:@"MyToolbarSplitView" bundle:nil];
    nextSplitViewController.title = @"Split view controller's title text";
    nextSplitViewController.sideViewDisplayOption = 
        SideViewDisplayOptionHideInPortraitOrientation;
    nextSplitViewController.listDisplayStyle = ListDisplayStyleSlideIn;
    UIViewController* mainViewController = // Code to allocate and init main view controller.
    nextSplitViewController.mainViewController = mainViewController;
    UIViewController* sideViewController = // Code to allocate and init side view controller.
    nextSplitViewController.sideViewController = sideViewController;
    [mainViewController release];
    [sideViewController release];
    
    for (UIBarButtonItem* nextItem in ((UIToolbar*) [nextSplitViewController.view viewWithTag:15]).items)
    {
        if (nextItem.tag==15)
        {
            // Found back-button.
            nextItem.target = self;
            nextItem.action = @selector(dismissModalView);
        }
        else if (nextItem.tag==16)
        {
            // Found greet button.
            nextItem.target = mainViewController;
            nextItem.action = @selector(sayHi);
        }
    }
    
    [myTopViewController presentModalViewController:nextSplitViewController animated:YES];
    [nextSplitViewController release];   

    In that example, MyToolbarSplitView is the name of the custom XIB file that we created.
    The listDisplayStyle property controls how the side view is displayed when the list button is pressed. The side view can be displayed in a popup view (the default), or it can slide in over the main view, or it can slide in, pushing the main view aside.

History

  • 2011/12/28 
    • Initial version. 
  • 2012/10/29  
    • In the test project, the split view controller is now explicitly assigned as the root view controller of the main window. With this change, the test project now works correctly in iOS 6.0 as well. The sample code in the article has been updated with the improvement.
  • 2012/12/18
    • When splitPoint was set programmatically, the view splitter did not re-appear when changing from portrait to landscape orientation. This bug was fixed. 
    • Fixed bug in orientation-change animation that manifested in some UI-configurations, which caused the side view to appear or disappear instantly. 
    • When binding SplitViewController to a custom xib, the view splitter is no longer assumed to have a width of 1. It can now have an arbitrary width. 
  • 2012/12/24 
    • When the list button is pressed in portrait orientation, the side view can now be displayed either in a popup view, as before, or it can slide in over the main view, similar to the way the Mail app works. This behaviour is controlled via the new listDisplayStyle property.
    • Note that the hideSideViewControllerInPortraitOrientation and sideViewControllerHiddenLeftButtonsImage properties have now been deprecated and may be removed from the SplitViewController class soon. If necessary, update your code to use hideSideViewInPortraitOrientation and sideViewHiddenLeftButtonsImage instead.
  • 2013/01/20
    Only now has the split view controller really become versatile!
    • A new list display style has been added. The ListDisplayStyleSlideIn style causes the list display to slide in, pushing the main view aside.
    • The hideSideViewInPortraitOrientation property has been replaced with the new sideViewDisplayOption property. This property provides three options: always display the side view (the default), hide side view in portrait orientation, or hide side view by default (the side view will be shown only when the list button is pressed).
    • The new sideViewPosition property controls if the side is shown on the left side or on the right side of the main view. 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)