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:
Here is what it looks like in portrait orientation, after pressing the list button, using the popover style:
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.
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
IBOutlet
s 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 =
topViewController.mainViewController = mainViewController;
UIViewController* sideViewController =
topViewController.sideViewController = sideViewController;
[sideViewController release];
[mainViewController release];
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 =
nextSplitViewController.mainViewController = mainViewController;
UIViewController* sideViewController =
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:
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 UIBarItem
s to the UIToolbar
if desirable).
Set the class of the File's Owner to SplitViewController
and connect the viewSplitter
, toolbar
,
and toolbarListButton
IBOutlet
s. 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 =
nextSplitViewController.mainViewController = mainViewController;
UIViewController* sideViewController =
nextSplitViewController.sideViewController = sideViewController;
[mainViewController release];
[sideViewController release];
for (UIBarButtonItem* nextItem in ((UIToolbar*) [nextSplitViewController.view viewWithTag:15]).items)
{
if (nextItem.tag==15)
{
nextItem.target = self;
nextItem.action = @selector(dismissModalView);
}
else if (nextItem.tag==16)
{
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
- 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.