Click here to Skip to main content
Click here to Skip to main content

Versatile, programmer-friendly Split View Controller for iOS

By , 20 Jan 2013
 

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:

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: 

    - (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:

    @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:
  • 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.
  • 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)

About the Author

Bernhard Häussermann
Software Developer Microworks, Blue Owl Software
South Africa South Africa
Member
Bernhard holds an Hons BSc in Computer Science and is a full-time software developer at Microworks, where he works mainly with Java, C# (.NET), and Pentaho Data Integration (aka Kettle).
 
After-hours he does iOS and Mac OS X development for Blue Owl Software. He also enjoys playing the piano.

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
QuestionIntegrating SplitView controller into existing projectmemberDavid DelMonte23 Feb '13 - 21:14 
Bernhard, I am integrating this controller into an existing project that has root and detail table views.
 
Everything is working except i cannot add/edit any cells. Any ideas / help would be much appreciated..
 
Thanks
AnswerRe: Integrating SplitView controller into existing projectmemberBernhard Häussermann24 Feb '13 - 5:02 
Hi David,
 
from the information that you've given me, I cannot tell what the cause of the problem might be.
I suspected that sometimes the touch-events received by the split-view are not propagated to the side- and main views. However, in my tests it looks as though the touch-events are always propagated correctly.
 
If you send me your source code I might be able to help you further.
GeneralRe: Integrating SplitView controller into existing projectmemberDavid DelMonte24 Feb '13 - 5:08 
That's generous Bernhard. Where would I send it and can I just send the relevant sections.. It's a project that's getting quite complex. Sniff | :^)
GeneralRe: Integrating SplitView controller into existing projectmemberBernhard Häussermann24 Feb '13 - 5:12 
You can send the code via email by email-replying to this comment. If you send just the relevant sections that would be fine.
QuestionRe: Integrating SplitView controller into existing projectmemberDavid DelMonte24 Feb '13 - 5:17 
Bernhard, sorry to be a pain.. Some of the code is proprietary, my email is ddelmonteatmacdotcom. If you email me offlist, I can attach most of the project. I am happy to post the solution for other here.
QuestionSplit View Controller - leak warningsmemberDavid DelMonte21 Feb '13 - 1:38 
Hi again Bernhard. I am integrating your split view controller into my project. For me, its necessary to allow the split vc not to be the top controller and your code is perfect for this. I am using iOS 6.1.
 
I am getting two leak warnings:
 
here: SplitViewController.m
 
- (void) imageView:(UIImageView*) imageView pressedWithTouches:(NSSet*) touches
...
  [barButtonItem.target performSelector:barButtonItem.action];
 
and here: MyImageView.m
 
- (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    [delegate performSelector:selector withObject:self withObject:touches];
}
 
I wouldn't be surprised if I caused these. However, can you replicate? Any ideas welcome..
 
Thanks
 
David
AnswerRe: Split View Controller - leak warningsmemberBernhard Häussermann21 Feb '13 - 5:33 
Hi David,
 
when I run Analyze through Xcode, it does not point me to any one of those two issues.
 
Furthermore, I cannot see how those lines of code might cause leaks.
GeneralRe: Split View Controller - leak warningsmemberDavid DelMonte21 Feb '13 - 8:07 
OK. Thanks Bernhard, it must be something I did. I'll download a fresh copy and compare. Thanks a lot for checking..
 
David
GeneralRe: Split View Controller - leak warningsmemberDavid DelMonte21 Feb '13 - 9:47 
Bernhard, I know what happened. I caused these warnings when I tried to convert the project to use ARC. I have the code interspersed with my own, so I don't know if this will work for me.
GeneralMy vote of 5groupevan89719 Feb '13 - 1:32 
Very Good article !!
GeneralLooking for some guidancememberThe5th18 Feb '13 - 9:14 
Hay B,
 
Nice project man! I been trying to get some basic principles behind me and your project was very good reference point.
 

Thx for sharing
 
Ciao
D
==========================
Since light travels faster than sound, some falk may look bright until you hear them speak.

BugWeird behavior setting splitPointmemberMassimo Colurcio11 Dec '12 - 10:38 
Hi Bernhard,
your class is simply awesome! 5 for your job.
 
I have found this weird behavior: setting the splitPoint and rotating the screen from landscape to portrait (hiding the left side view) everything seems perfect... but when I rotate the screen back (from portrait to landscape) the black line between the two view disappears.
This does not happens when splitPoint is not set.
 
for a quick test add this line
[nextSplitViewController setSplitPoint:300];
in MainMenuViewController.m [line 191].
 
Run the simulator, tap on Show navigation view controller... rotate the screen and see the results Confused | :confused: .
 
Thanks,
Massimo
GeneralRe: Weird behavior setting splitPointmemberBernhard Häussermann11 Dec '12 - 23:17 
Hi Massimo,
 
that is interesting.
Thank you for your feedback and the method to reproduce the problem! I will try it out tonight and see if I can fix it.
GeneralRe: Weird behavior setting splitPointmemberBernhard Häussermann12 Dec '12 - 6:59 
Hi Massimo,
 
I tested on iPad 5.1 Simulator and I could not produce the problem.
Which iOS version are you running?
GeneralRe: Weird behavior setting splitPointmemberMassimo Colurcio12 Dec '12 - 9:45 
I tested on iPad 6.0 simulator
GeneralRe: Weird behavior setting splitPointmemberBernhard Häussermann13 Dec '12 - 4:29 
Hi Massimo,
 
it looks as though this issue occurs since iOS 6.
Unfortunately I cannot produce the problem since I'm still on iOS 5.1. I guess this is a good excuse to download the new Xcode, and upgrade my devices! I will start the download so long and upgrade and debug the issue over the weekend.
AnswerRe: Weird behavior setting splitPointmemberBernhard Häussermann16 Dec '12 - 0:04 
Hi Massimo,
 
I realised that the issue actually does also occur on iOS 5 (I just didn't notice it when I tested last).
 
The problem was that I didn't properly think through the details of the coordinates of the different views. As a result, the type of scenario described by you caused the split view to end up behind the side view, making it invisible. I have updated the coordinates as documented in this image (the thin, black box represents the split view).
 
I will update the article with the new code soon. In the meanwhile, you can download the new code here.
GeneralRe: Weird behavior setting splitPointmemberMassimo Colurcio16 Dec '12 - 11:15 
Superb. Now it works like a charm.
 
Tnx
QuestionThe SplitViewController gets rotated perfectly in iOS 5.0 but not in iOS6??? [modified]memberMember 953691322 Oct '12 - 15:15 
When running the test project in iOS6 simulator, the following message comes out: "SplitViewControllerTestProject[445:c07] Application windows are expected to have a root view controller at the end of application launch"
 
Then, the various shouldAutorotateToInterfaceOrientation: does not get called and the simulator sticks in portrait mode. Anything I can work around? Thanks for advance.
 
[Edit 20121023 10:16]
Just figured out that adding the following code snippets in AppDelegate before both the sideViewController and mainViewController get released will make the rotation works:
 
[self.window setRootViewController:topViewController];
 
As I am quite new to iOS programming, will there be any side effect doing so?
[End Edit

modified 22 Oct '12 - 22:18.

AnswerRe: The SplitViewController gets rotated perfectly in iOS 5.0 but not in iOS6???memberBernhard Häussermann23 Oct '12 - 7:35 
Hello Member,
 
thank you for the feedback!
 
I think that the solution that you suggested is the correct approach.
In the test project, I have now replaced the line
 
[self.window addSubview:topViewController.view];
 
with
 
self.window.rootViewController = topViewController;
 
and will update the code of this article soon.
 
There should be no side effect; in fact, it probably should have been this way in the first place.
 
It looks like when adding a single UIView as a sub-view to a UIWindow prior to iOS 6, UIWindow considered the UIViewController of the UIView to be the root view controller by default (I do not know, though, how UIWindow managed to access the UIViewController).
Apparently, it no longer works that way in iOS 6, so the root view controller must be assigned explicitly. Assigning the root view controller, automatically adds its UIView as a sub-view to the UIWindow, so there's no more need to add it explicitly.
Questionsplit screen verticalymemberMember 92039273 Jul '12 - 11:09 
Hi,
 
Can you explain how to modify the code to split the screen vertically instead of horizontly?
 
Thanks
AnswerRe: split screen verticalymemberBernhard Häussermann4 Jul '12 - 4:14 
Hi guest,
 
I assume you want the screen to be split in both orientations. Just set the hideSideViewControllerInPortraitOrientation property to NO.
QuestionHellomemberRhez Ann29 Jun '12 - 0:32 
The tutorial looks great! But I would like to ask some questions if the portrait view would be the same as its view in landscape mode without using popovers?! Thanks..
AnswerRe: HellomemberBernhard Häussermann30 Jun '12 - 8:07 
Hello Rhez,
 
the value of the hideSideViewControllerInPortraitOrientation property of the split view controller defaults to YES, which causes the side view controller to appear in a popover in portrait orientation. To have it appear the same as in landscape orientation, simply set the value of the hideSideViewControllerInPortraitOrientation property to NO.
QuestionSub view controllermemberÖzgür Akdemirci8 Mar '12 - 22:30 
Thanks for your effort. UISplitViewController has the limitation of being the root of any interface you create. Your implementation mandates this or can I use it for a subview controller in my tab controller. I'll try asap. Thanks again. Big Grin | :-D

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web01 | 2.6.130516.1 | Last Updated 20 Jan 2013
Article Copyright 2011 by Bernhard Häussermann
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid