Beginning iPhone Development: Exploring the iOS SDK, Seventh Edition (2014)
Chapter 11. Using Split Views and Popovers
In Chapter 9, you spent a lot of time dealing with app navigation based on selections in table views, where each selection causes the top-level view, which fills the entire screen, to slide to the left and bring in the next view in the hierarchy (or perhaps yet another table view). Plenty of iPhone and iPod touch apps work this way, including some of Apple’s own apps. One typical example is Mail, which lets you drill down through mail accounts and folders until you finally make your way to a message. Technically, this approach can work on the iPad as well, but it leads to a user interaction problem.
On a screen the size of the iPhone or iPod touch, having a screen-sized view slide away to reveal another screen-sized view works well. On a screen the size of the iPad, however, that same interaction feels a little wrong, a little exaggerated, and even a little overwhelming. In addition, consuming such a large display with a single table view is inefficient in most cases. As a result, you’ll see that the built-in iPad apps do not actually behave that way. Instead, any drill-down navigation functionality, like that used in Mail, is relegated to a narrow column whose contents slide left or right as the user drills down or backs out. With the iPad in landscape mode, the navigation column is in a fixed position on the left, with the content of the selected item displayed on the right. This is what’s called a split view (see Figure 11-1) and applications built this way are calledmaster-detail applications.
Figure 11-1. This iPad, in landscape mode, is showing a split view. The navigation column is on the left. Tap an item in the navigation column—in this case, a specific mail account—and that item’s content is displayed in the area on the right
The split view is perfect for developing master-detail applications like the Mail app. Prior to iOS 8, the split view class (UISplitViewController) was only available on the iPad, which meant that if you wanted to build a universal master-detail application, you had to do it one way on the iPad and another way on the iPhone. Now, UISplitViewController is also available everywhere, which means that you no longer need to write special code to handle the iPhone.
When used on the iPad, the left side of the split view is 320 points wide by default, which is the same width as an iPhone in its vertical position. The split view itself, with navigation and content side by side, typically appears only in landscape mode. If you turn the device to portrait orientation, the split view is still in play, but it’s no longer visible in the same way. The navigation view loses its permanent location and can be activated only by swiping in from the left side of the view or pressing a toolbar button, which causes it to slide in from the left, in a view that floats in front of everything else on the screen (see Figure 11-2).
Figure 11-2. This iPad, in portrait mode, does not show the same split view as seen in landscape mode. Instead, the information that made up the left side of the split view in landscape mode appears only when the user swipes in from the left side of the split view or taps a toolbar button
Some applications don’t follow this rule strictly, though. The iPad Settings app, for instance, uses a split view that is visible all the time, and the left side neither disappears nor covers the content view on the right. In this chapter, however, we’ll stick to the standard usage pattern.
In this chapter’s example project, you’ll see how to create a master-detail application that uses a split view controller. Initially, we’ll test the application on the iPad simulator, but when it’s finished, you’ll see that the same code also works on the iPhone, although it doesn’t quite look the same. You’ll also learn how to customize the split view’s appearance and behavior, and how to create and display a popover that’s like the one that you saw in Chapter 4 when we discussed alert views and action sheets. Unlike the popover in Figure 4-28, which wrapped an action sheet, this one will contain content that is specific to the example application—specifically, a list of languages (see Figure 11-3).
Figure 11-3. A popover, which visually seems to sprout from the button that triggered its appearance
Building Master-Detail Applications with UISplitViewController
We’re going to start off with an easy task: taking advantage of one of Xcode’s predefined templates to create a split view project. We’ll build an app that lists all the US presidents and shows the Wikipedia entry for whichever one you select.
Go to Xcode and select File New Project. . .. From the iOS Application section, select Master-Detail Application and click Next. On the next screen, name the new project Presidents, set the Language to Objective-C and Devices to Universal. Make sure that the Use Core Datacheck box is unchecked. Click Next, choose the location for your project, and then click Create. Xcode will do its usual thing, creating a handful of classes and a storyboard file for you, and then showing the project. If it’s not already open, expand the Presidents folder and take a look at what it contains.
From the start, the project contains an app delegate (as usual), a class called MasterViewController, and a class called DetailViewController. Those two view controllers represent, respectively, the views that will appear on the left and right sides of the split view in landscape orientation. MasterViewController defines the top level of a navigation structure and DetailViewController defines what’s displayed in the larger area when a navigation element is selected. When the app launches, both of these are contained inside a split view, which, as you may recall, does a bit of shape-shifting as the device is rotated.
To see what this particular application template gives you in terms of functionality, build the app and run it in the iPad simulator (the application works on the iPhone too, but its behavior is slightly different, so we’ll defer discussing that aspect of the split view controller until later in the chapter.) If the application launches into portrait mode, you’ll see just the detail view controller, as shown on the left in Figure 11-4. Tap the Master button on the toolbar or swipe from the left edge of the view to the right to slide in the master view controller over the top of the detail view, as shown on the right in Figure 11-4.
Figure 11-4. The default master-detail application in portrait mode. The layout on the right is similar to Figure 11-2
Rotate the simulator (or device) left or right, into landscape mode. In this mode, the split view works by showing the navigation view on the left and the detail view on the right (see Figure 11-5).
Figure 11-5. The default master-detail application in landscape mode. Note the similar layouts shown in this figure and Figure 11-1
We’re going to build on this to make the president-presenting app, but first let’s dig into what’s already there.
The Storyboard Defines the Structure
Right off the bat, you have a pretty complex set of view controllers in play:
· A split view controller that contains all the elements
· A navigation controller to handle what’s happening on the left side of the split
· A master view controller (displaying a master list of items) inside the navigation controller
· A detail view controller on the right
· Another navigation controller as a container for the detail view controller on the right
In the default master-detail application template that we used, these view controllers are set up and interconnected primarily in the main storyboard file, rather than in code. Apart from doing GUI layout, Interface Builder really shines as a way of letting you connect different components without writing a bunch of code just to establish relationships. Let’s dig into the project’s storyboard to see how things are set up.
Select Main.storyboard to open it in Interface Builder. This storyboard really has a lot of stuff going on. You’ll definitely want to open the Document Outline for the best results (see Figure 11-6). Zooming out (by right-clicking the storyboard editor and choosing a magnification level from the pop-up) can also help you see the big picture.
Figure 11-6. MainStoryboard.storyboard open in Interface Builder. This complex object hierarchy is best viewed in the Document Outline
To get a better sense of how these controllers relate to one another, open the Connections Inspector, and then spend some time clicking each of the view controllers in turn. Here’s a quick summary of what you’ll find:
· The UISplitViewController has relationship segues called master view controller and detail view controller to two UINavigationControllers. These are used to tell the UISplitViewController what it should use for the narrow strip it displays on the left (the master view controller), as well as what it should use for the larger display area (the detail view controller).
· The UINavigationController linked via the master view controller segue has a root view controller relationship to its own root view controller, which is the MasterViewController class generated by the template. The master view controller is a subclass of UITableViewController, which you should be familiar with from Chapter 9.
· Similarly, the other UINavigationController has a root view controller relationship to the detail view controller, which is the template’s DetailVIewController class. The detail view controller generated by the template is a plain UIViewControllersubclass, but you are at liberty to use any view controller that meets your application’s requirements.
· There is a storyboard segue from the cells in the master view controller to the detail view controller, of type showDetail. This segue causes the item in the clicked cell to be shown in the detail view. More about this later when we take a more detailed look at the master view controller.
At this point, the content of Main.storyboard is really a definition of how the app’s various controllers are interconnected. As in most cases where you’re using storyboards, this eliminates a lot of code, which is usually a good thing. If you’re the kind of person who likes to see all such configuration done in code, you’re free to do so; but for this example, we’re going to stick with what Xcode has provided.
The Code Defines the Functionality
One of the main reasons for keeping the view controller interconnections in a storyboard is that they don’t clutter up your source code with configuration information that doesn’t need to be there. What’s left is just the code that defines the actual functionality.
Let’s look at what we have as a starting point. Xcode defined several classes for us when the project was created, and we’re going to peek into each of them before we start making any changes.
The App Delegate
First up is AppDelegate.h, which looks something like this:
#import <UIKit/UIKit.h>
@interface AppDelegate : UIResponder <UIApplicationDelegate>
@property (strong, nonatomic) UIWindow *window;
@end
This is pretty similar to several other application delegates you’ve seen in this book so far. Now switch over to the implementation in AppDelegate.m. The code at the start of this file looks something like the following (most comments and empty methods have been deleted here for the sake of brevity):
#import "AppDelegate.h"
#import "DetailViewController.h"
@interface AppDelegate () <UISplitViewControllerDelegate>
@end
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
UISplitViewController *splitViewController =
(UISplitViewController *)self.window.rootViewController;
UINavigationController *navigationController =
[splitViewController.viewControllers lastObject];
navigationController.topViewController.navigationItem.leftBarButtonItem =
splitViewController.displayModeButtonItem;
splitViewController.delegate = self;
return YES;
}
Let’s look at the last part of this code first:
splitViewController.delegate = self;
This line sets the UISplitViewController’s delegate property, pointing it at the application delegate itself. Later in this chapter, when we look at how split views behave on the iPhone, we’ll see why this delegate connection is required. But why make this connection here in code, instead of having it hooked up directly in the storyboard? After all, just a few paragraphs ago, you were told that elimination of boring code—“connect this thing to that thing”—is one of the main benefits of both nibs and storyboards. And we’ve hooked up delegates in Interface Builder plenty of times, so why can’t we do that here?
To understand why using a storyboard to make the connections can’t really work here, you need to consider how a storyboard differs from a nib file. A nib file is really a frozen object graph. When you load a nib into a running application, the objects it contains all “thaw out” and spring into existence, including all the interconnections specified in the file. The system creates a fresh instance of every single object in the file, one after another, and connects all the outlets and connections between objects.
A storyboard, however, is something more than that. You could say that each scene in a storyboard corresponds roughly to a nib file. When you add in the metadata describing how the scenes are connected via segues, you end up with a storyboard. However, unlike a single nib, a complex storyboard is not normally loaded all at once. Instead, any activity that causes a new scene to be activated will end up loading that particular scene’s frozen object graph from the storyboard. This means that the objects you see when looking at a storyboard won’t necessarily all exist at the same time.
Since Interface Builder has no way of knowing which scenes will coexist, it actually forbids you from making any outlet or target/action connections from an object in one scene to an object in another scene. In fact, the only connections it allows you to make from one scene to another are segues.
But don’t take our word for it, try it out yourself! First, select the Split View Controller in the storyboard (you’ll find it within the dock in the Split View Controller Scene). Now bring up the Connections Inspector and try to drag a connection from the delegate outlet to another view controller or object. You can drag all over the layout view and the list view, and you won’t find any spot that highlights (which would indicate it was ready to accept a drag). The only way to make this connection is in code. All in all, this extra bit of code is a small price to pay, considering how much other code is eliminated by our use of storyboards.
Now let’s rewind and look at what happens at the start of the application:didFinishLaunchingWithOptions: method:
UISplitViewController *splitViewController =
(UISplitViewController *)self.window.rootViewController;
This grabs the window’s rootViewController, which is the one indicated in the storyboard by the free-floating arrow. If you look back at Figure 11-6, you’ll see that the arrow points at our UISplitViewController instance. This code comes next:
UINavigationController *navigationController =
[splitViewController.viewControllers lastObject];
On this line, we dig into the UISplitViewController’s viewControllers array. When the split view is loaded from the storyboard, this array has references to the navigation controllers wrapping the master and detail view controllers. We grab the last item in this array, which points to the UINavigationController for our detail view. Finally, we see this:
navigationController.topViewController.navigationItem.leftBarButtonItem =
splitViewController.displayModeButtonItem;
This assigns the displayModeButtonItem of the split view controller to the navigation bar of the detail view controller. The displayModeButtonItem is a bar button item that is created and managed by the split view itself. This code is actually adding the Master button that you can see on the navigation bar on the left in Figure 11-4. On the iPad, the split view shows this button when the device is in portrait mode and the master view controller is not visible. When the device rotates to landscape orientation or the user presses the button to make the master view controller visible, the button is hidden. You’ll see later that this button is also used on the iPhone to allow the user to manually show and hide the master view controller.
The Master View Controller
Now, let’s take a look at MasterViewController, which controls the setup of the table view containing the app’s navigation. MasterViewController.h looks like this:
#import <UIKit/UIKit.h>
@class DetailViewController;
@interface MasterViewController : UITableViewController
@property (strong, nonatomic) DetailViewController *detailViewController;
@end
Its corresponding MasterViewController.m file starts off like this (we’re just looking at the first few methods now and will deal with the rest later):
#import "MasterViewController.h"
#import "DetailViewController.h"
@interface MasterViewController ()
@property NSMutableArray *objects;
@end
@implementation MasterViewController
- (void)awakeFromNib {
[super awakeFromNib];
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
self.clearsSelectionOnViewWillAppear = NO;
self.preferredContentSize = CGSizeMake(320.0, 600.0);
}
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
self.navigationItem.leftBarButtonItem = self.editButtonItem;
UIBarButtonItem *addButton = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self
action:@selector(insertNewObject:)];
self.navigationItem.rightBarButtonItem = addButton;
self.detailViewController = (DetailViewController *)
[[self.splitViewController.viewControllers lastObject] topViewController];
}
.
@end
A fair amount of configuration is happening here. Fortunately, Xcode provides all of this code as part of the split view template. First, the awakeFromNib method starts like this:
- (void)awakeFromNib {
[super awakeFromNib];
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
self.clearsSelectionOnViewWillAppear = NO;
The if statement gets the user interface idiom from the UIDevice object that represents the device on which the application is running and tests whether it’s an iPad. If it is, it sets the view controller’s clearsSelectionOnViewWillAppear property to NO. This property is defined by the UITableViewController class (which is the superclass of MasterViewController) and lets us tweak the controller’s behavior a bit. By default, UITableViewController is set up to deselect all rows each time it’s displayed. That may be OK in an iPhone app, where each table view is usually displayed on its own; however, in an iPad app featuring a split view, you probably don’t want that selection to disappear. To revisit an earlier example, consider the Mail app. The user selects a message on the left side and expects that selection to remain there, even if the message list disappears (due to rotating the iPad or closing the popover containing the list). This line fixes that.
Next, the awakeFromNib method sets the view’s preferredContentSize property. That property sets the size of the view if this view controller should happen to be used to provide the display for some other view controller that allows a variable size. In this case, it’s intended to be used when the master view controller is displayed in portrait mode. Although this property is set here, in iOS 8 it does not appear to have any effect—you’ll see the correct way to control the width of the master view controller in portrait mode in “Customizing the Split View” later in this chapter.
The final point of interest here is the viewDidLoad method. In previous chapters, when you implemented a table view controller that responds to a user row selection, you typically responded to the user selecting a row by creating a new view controller and pushing it onto the navigation controller’s stack. In this app, however, the view controller we want to show is already in place, and it will be reused each time the user makes a selection on the left. It’s the instance of DetailViewController contained in the storyboard file. Here, we’re grabbing thatDetailViewController instance and hanging saving it in a property, anticipating that we’ll want to use it later. However, this property is not used in the rest of the template code.
The viewDidLoad method also adds a button to the toolbar. This is the + button that you can see on the right of master view controller’s navigation bar in Figure 11-4 and Figure 11-5. The template application uses this button to create and add a new entry to the master view controller’s table view. Since we don’t need this button in our Presidents application, we’ll be removing this code shortly.
There are several more methods included in the template for this class, but don’t worry about those right now. We’re going to delete some of those and rewrite the others, but only after taking a detour through the detail view controller.
The Detail View Controller
The final class created for us by Xcode is DetailViewController, which takes care of the actual display of the item the user chooses from the table in the master view controller. Here’s what DetailViewController.h looks like:
#import <UIKit/UIKit.h>
@interface DetailViewController : UIViewController
@property (strong, nonatomic) id detailItem;
@property (weak, nonatomic) IBOutlet UILabel *detailDescriptionLabel;
@end
This is very straightforward—the detailItem property is where the view controller stores its reference to the object that the user selected in the master view controller, and detailDescriptionLabel is an outlet that connects to a label in the storyboard. In the template application, the label simply displays a description of the object in the detailItem property.
Switch over to DetailViewController.m, where you’ll find the following:
#import "DetailViewController.h"
@interface DetailViewController ()
@end
@implementation DetailViewController
#pragma mark - Managing the detail item
- (void)setDetailItem:(id)newDetailItem {
if (_detailItem != newDetailItem) {
_detailItem = newDetailItem;
// Update the view.
[self configureView];
}
}
- (void)configureView {
// Update the user interface for the detail item.
if (self.detailItem) {
self.detailDescriptionLabel.text = [self.detailItem description];
}
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
[self configureView];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
@end
The most important thing in this class is this method:
- (void)setDetailItem:(id)newDetailItem {
if (_detailItem != newDetailItem) {
_detailItem = newDetailItem;
// Update the view.
[self configureView];
}
}
The setDetailItem: method may seem surprising to you. We did, after all, define detailItem as a property, and the compiler would automatically create the getter and setter for us, so why create a setter ourselves? In this case, we need to be able to react whenever the setter is called (we’ll see exactly how this happens in the “How the Master-Detail Template Application Works” section later in this chapter), so that we can update the display. Implementing the setter ourselves is the easiest way to do that.
The first part of the method just stores the new property value in the instance variable that the compiler creates for us. Then, the configureView method is called. This is another method that’s generated for us. All it does is call the description method of the detail object and then uses the result to set the text property of the label in the storyboard:
- (void)configureView {
// Update the user interface for the detail item.
if (self.detailItem) {
self.detailDescriptionLabel.text = [self.detailItem description];
}
}
The description method is implemented by every subclass of NSObject. If your class doesn’t override it, it returns a default value that’s probably not very useful. However, in this example, the detail objects are all instances of the NSDate class and NSDate’s implementation of thedescription method returns the date and time, formatted in a generic way.
How the Master-Detail Template Application Works
Now you’ve seen all of the pieces of the template application, but you’re probably still not very clear on how it works, so let’s run it and take a look at what it actually does.
Run the application on an iPad simulator and rotate the device to landscape mode so that the master view controller appears. You can see that the label in the detail view controller currently has the default text that’s assigned to it in the storyboard. What we’re going to see in this section is how the act of selecting an item in the master view controller causes that text to change. There currently aren’t any items in the master view controller. To fix that, press the + button at the top right of its navigation bar a few times. Every time you do that, a new item is added to the controller’s table view, as shown in Figure 11-7.
Figure 11-7. The template application with an item selected in the master view controller and displayed in the detail view controller
All of the items in the master view controller table are dates. Select one of them, and the label in the detail view updates to show the same date. You’ve already seen the code that does this—it’s the configureView method in DetailViewController.m, which is called when a new value is stored in the detail view controller’s detailItem property. What is it that causes a new property value to be set?
Take a look back at the storyboard in Figure 11-6. There’s a segue that links the prototype table cell in the master view controller’s table cell to the detail view controller. If you click this segue and open the Attributes Inspector, you’ll see that this is a Show Detail segue with the identifiershowDetail (see Figure 11-8).
Figure 11-8. The Show Detail segue linking the master and detail view controllers
As you saw in Chapter 9, a segue that’s linked to a table view cell is triggered when that cell is selected, so when you select a row in the master view controller’s table view, iOS performs the Show Detail segue, with the navigation controller wrapping the detail view controller as the segue destination. This causes two things to happen:
· A new instance of the detail view controller is created and its view is added to the view hierarchy.
· The prepareForSegue:sender: method in the master view controller is called.
The first step takes care of making sure the detail view controller is visible. In the second step, your master view controller needs to display the object selected in the master view controller in some way. Here’s how the template code in MasterViewController.m handles this:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([[segue identifier] isEqualToString:@"showDetail"]) {
NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
NSDate *object = self.objects[indexPath.row];
DetailViewController *controller =
(DetailViewController *)[[segue destinationViewController] topViewController];
[controller setDetailItem:object];
controller.navigationItem.leftBarButtonItem = self.splitViewController. displayModeButtonItem;
controller.navigationItem.leftItemsSupplementBackButton = YES;
}
}
First, the segue identifier is checked to make sure that it’s the one that is expected and that the NSDate object from the selected object in the view controller’s table is obtained. Next, the master view controller finds the DetailViewController instance from thetopViewController property of the destination view controller in the segue that caused this method to be called. Now that we have both the selected object and the detail view controller, all we have to do is set the detail view controller’s detailItem property to cause the detail view to be updated. The final two lines of the prepareForSegue:sender: method add the display mode button to the detail view controller’s navigation bar. When the device is in landscape mode, this doesn’t do anything because the display mode button isn’t visible, but if you rotate to portrait orientation, you’ll see that the button (it’s the Master button) appears.
So now you know how the selected item in the master view controller gets displayed in the detail view controller. Although it doesn’t look like much is going on here, in fact there is a great deal happening under the hood to make this work correctly on both the iPad and the iPhone, in portrait and landscape orientations. The beauty of the split view controller is that it takes care of all the details and leaves you free to worry about how to implement your custom master and detail view controllers.
That concludes the overview of what Xcode’s Master-Detail Application template gives you. It might be a lot to absorb at a glance, but, ideally, presenting it a piece at a time has helped you understand how all the pieces fit together.
Here Come the Presidents
Now that you’ve seen the basic layout of our project, it’s time to fill in the blanks and turn the template app into something all your own. Start by looking in the book’s source code archive, where the folder 11 – Presidents Data contains a file called PresidentList.plist. Drag that file into your project’s Presidents folder in Xcode to add it to the project, making sure that the check box telling Xcode to copy the file itself is checked. This .plist file contains information about all the US presidents so far, consisting of just the name and the Wikipedia entry URL for each of them.
Now, let’s look at the master view controller and see how we need to modify it to handle the presidential data properly. It’s going to be a simple matter of loading the list of presidents, presenting them in the table view, and passing a URL to the detail view for display. InMasterViewController.m, start off by adding the bold line shown here to the class extension and removing the crossed-out line:
@interface MasterViewController ()@property NSMutableArray *objects;
@property (copy, nonatomic) NSArray *presidents;
@end
Instead of holding our list of presidents in the mutable array that was created by Xcode, we create our own immutable array with a more meaningful name.
Now divert your attention to the viewDidLoad method, where the changes are a little more involved (but still not too bad). You’re going to add a few lines to load the list of presidents, and then remove a few other lines that set up edit and insertion buttons in the toolbar:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
self.navigationItem.leftBarButtonItem = self.editButtonItem;
NSString *path = [[NSBundle mainBundle] pathForResource:@"PresidentList"
ofType:@"plist"];
NSDictionary *presidentInfo = [NSDictionary
dictionaryWithContentsOfFile:path];
self.presidents = [presidentInfo objectForKey:@"presidents"];
UIBarButtonItem *addButton = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self
action:@selector(insertNewObject:)];
self.navigationItem.rightBarButtonItem = addButton;
self.detailViewController = (DetailViewController *)
[[self.splitViewController.viewControllers lastObject] topViewController];
}
This template-generated class also includes a method called insertNewObject: for adding items to the objects array. We don’t even have that array anymore, so we delete the entire method:
- (void)insertNewObject:(id)sender {
if (!_objects) {
_objects = [[NSMutableArray alloc] init];
}
[_objects insertObject:[NSDate date] atIndex:0];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
[self.tableView insertRowsAtIndexPaths:@[indexPath]
withRowAnimation:UITableViewRowAnimationAutomatic];}
Also, we have a couple of data source methods that deal with letting users edit rows in the table view. We’re not going to allow any editing of rows in this app, so let’s just remove this code before adding our own:
- (BOOL)tableView:(UITableView *)tableView
canEditRowAtIndexPath:(NSIndexPath *)indexPath {
// Return NO if you do not want the specified item to be editable.
return YES;}- (void)tableView:(UITableView *)tableView
commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
[self.objects removeObjectAtIndex:indexPath.row];
[tableView deleteRowsAtIndexPaths:@[indexPath]
withRowAnimation:UITableViewRowAnimationFade];
} else if (editingStyle == UITableViewCellEditingStyleInsert) {
// Create a new instance of the appropriate class, insert it into the array,
and add a new row to the table view.
}}
Now it’s time to get to the main table view data source methods, adapting them for our purposes. Let’s start by editing the method that tells the table view how many rows to display:
- (NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section {
return self.objects.count;
return [self.presidents count];
}
After that, edit the tableView:cellForRowAtIndexPath: method to make each cell display a president’s name:
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell =
[tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];
NSDate *object = self.objects[indexPath.row];
cell.textLabel.text = [object description];
NSDictionary *president = self.presidents[indexPath.row];
cell.textLabel.text = president[@"name"];
return cell;
}
Finally, edit the prepareForSegue:sender: method to pass the data for the selected president (which is an NSDictionary) to the detail view controller, as follows:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([[segue identifier] isEqualToString:@"showDetail"]) {
NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
NSDate *object = self.objects[indexPath.row];
DetailViewController *controller =
(DetailViewController *)[[segue destinationViewController] topViewController];
[controller setDetailItem:object];
NSDictionary *president = self.presidents[indexPath.row];
controller .detailItem = president;
controller.navigationItem.leftBarButtonItem =
self.splitViewController.displayModeButtonItem;
controller.navigationItem.leftItemsSupplementBackButton = YES;
}
}
That’s all we need to do in the master view controller.
Next, select Main.storyboard and click the Master icon in the Master Scene in the Document Outline to select the master view controller, and then double-click its title bar and replace Master with Presidents and save the storyboard.
At this point, you can build and run the app. Switch to landscape mode, or tap the Master button in the upper-left corner to bring up the master view controller, showing a list of presidents (see Figure 11-9). Tap a president’s name to display a not-very-useful string in the detail view.
Figure 11-9. Our first run of the Presidents app, showing a list of presidents in the master view controller, but nothing useful in the detail view
Let’s finish this example by making the detail view do something a little more useful with the data that it’s given. Start with DetailViewContoller.h, where we’ll add an outlet for a web view to display the Wikipedia page for the selected president. Add the bold line shown here:
@interface DetailViewController : UIViewController
@property (strong, nonatomic) id detailItem;
@property (weak, nonatomic) IBOutlet UILabel *detailDescriptionLabel;
@property (weak, nonatomic) IBOutlet UIWebView *webView;
@end
Next, switch to DetailViewController.m, where we have a bit more to do (though really, not too much). Scroll down to the configureView method and replace it with the following code:
- (void)configureView {
// Update the user interface for the detail item.
if (self.detailItem) {
NSDictionary *dict = (NSDictionary *)self.detailItem;
NSString *urlString = dict[@"url"];
self.detailDescriptionLabel.text = urlString;
NSURL *url = [NSURL URLWithString:urlString];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[self.webView loadRequest:request];
NSString *name = dict[@"name"];
self.title = name;
}
}
The detailItem that was set by the master view controller is an NSDictionary containing two key-value pairs: one with a key name that stores the president’s name and another with a key url that gives the URL of the president’s Wikipedia page. We use the URL to set the text of the detail description label and to construct an NSURLRequest that the UIWebView will use to load the page. We use the name to set the detail view controller’s title. When a view controller is a container in a UINavigationController, the value in its title property is displayed in the navigation controller’s navigation bar. That’s all we need to get our web view to load the requested page.
The final changes we need to make are in Main.storyboard. Open it for editing and find the detail view at the lower right. Let’s first take care of the label in the GUI (the text of which reads, “Detail view content goes here”).
Start by selecting the label. You might find it easiest to select the label in the Document Outline, in the section labeled Detail Scene. Once the label is selected, drag it to the top of the window. The label should run from the left-to-right blue guideline and fit snugly under the navigation bar (resize it to make sure that is the case). This label is being repurposed to show the current URL. But when the application launches, before the user has chosen a president, we want this field to give the user a hint about what to do.
Double-click the label and change it to Select a President. You should also use the Size Inspector to make sure that the label’s position is constrained to both the left and right sides of its superview, as well as the top edge (see Figure 11-10). If you need to adjust these constraints, use the methods described earlier to set them up. You can probably get almost exactly what you want by selecting the label and then choosing Editor Resolve Auto Layout Issues Reset to Suggested Constraints from the menu.
Figure 11-10. The Size Inspector, showing the constraints settings for the “Select a President” label at the bottom
Next, use the library to find a UIWebView and drag it into the space below the label you just moved. After dropping the web view there, use the resize handles to make it fill the rest of the view below the label. Make it go from the left edge to the right edge, and from the blue guideline just below the bottom of the label all the way to the very bottom of the window. Now use the Size Inspector to constrain the web view to the left, bottom, and right edges of the superview, as well as to the label for the top edge (see Figure 11-11). Once again, you can probably get exactly what you need by selecting Editor Resolve Auto Layout Issues Reset to Suggested Constraints from the menu.
Figure 11-11. The Size Inspector, showing the constraints settings for the web view
Now select the Master view controller in the Document Outline and open the Attributes Editor. In the View Controller section, change the Title from Master to Presidents. This changes the title of the navigation button at the top of the detail view controller to something more useful.
We have one last step to complete. To hook up the outlet for the web view that you created, Control-drag from the Detail icon (in the Detail Scene section in the Document Outline) to our new Web View (same section, just below the label in the Document Outline, or in the storyboard), and connect the webView outlet. Save your changes, and you’re finished!
Now you can build and run the app, and it will let you see the Wikipedia entries for each of the presidents (see Figure 11-12). Rotate the display between the two orientations, and you’ll see how the split view controller takes care of everything for you, with a little help from the detail view controller.
Figure 11-12. The Presidents application, showing the Wikipedia page for George Washington
Creating Your Own Popover
Back in Chapter 4, you saw that you can display an action sheet in what looks like a cartoon speech bubble (see Figure 4-28). That speech bubble is the visual representation of a popover controller, or popover for short. The popover that you get with an action sheet is created for you when the action sheet is presented by a UIPopoverPresentationController, which you have very little control over. However, you can create your own popover by using the UIPopoverController class, which can come in handy when you want to present your own view controllers.
To see how this works, we’re going to add a popover to be activated by a permanent toolbar item (unlike the one in the UISplitView, which is meant to come and go). This popover will display a table view containing a list of languages. If the user picks a language from the list, the web view will load (in the new language) whatever Wikipedia entry that was already showing. This will be simple enough to do, since switching from one language to another in Wikipedia is just a matter of changing a small piece of the URL that contains an embedded country code. Figure 11-3shows what we are aiming for. It’s important to note, however, that UIPopoverController is available only on the iPad, so when this application is run on the iPhone, the language selector will be missing.
Note The use of a popover in this example is in the service of showing a UITableView, but don’t let that mislead you—UIPopoverController can be used to handle the display of any view controller content you like! We’re sticking with table views for this example because it’s a common use case, it’s easy to show in a relatively small amount of code, and it’s something with which you should already be quite familiar.
Start by right-clicking the Presidents folder in Xcode and selecting New File. . . from the pop-up menu. When the assistant appears, select Cocoa Touch Class from the iOS Source section, and then click Next. On the next screen, name the new class LanguageListController and selectUITableViewController from the Subclass of field. Click Next, double-check the location where you’re saving the file, and click Create.
The LanguageListController is going to be a pretty standard table view controller class. It will display a list of items and let the detail view controller know when a choice is made, by using a pointer back to the detail view controller. Edit LanguageListController.h, adding the bold lines shown here:
#import <UIKit/UIKit.h>
@class DetailViewController;
@interface LanguageListController : UITableViewController
@property (weak, nonatomic) DetailViewController *detailViewController;
@property (copy, nonatomic) NSArray *languageNames;
@property (copy, nonatomic) NSArray *languageCodes;
@end
These additions define a pointer back to the detail view controller (which we’ll set from code in the detail view controller itself when we’re about to display the language list), as well as a pair of arrays for containing the values that will be displayed (English, French, etc.) and the underlying values that will be used to build an URL from the chosen language (en, fr, and so on). Note that we’ve declared these arrays to have copy storage semantics instead of strong. This means that whenever some piece of code calls one of these setters, the parameter is sent a copy message instead of just being held as a strong pointer. This is done to prevent a situation where another class might send in an NSMutableArray instead of an NSArray, and then make changes to the array without our knowledge. Sending copy to an NSMutableArray instance always returns an immutable NSArray, so we know that the array we’re using can’t be changed by someone else. At the same time, sending copy to an NSArray, which is already immutable, doesn’t actually make a new copy, it just returns a strong pointer to self, so sending it a copy message isn’t wasteful in any way.
If you copied and pasted this code from the book’s source archive (or e-book) into your own project or typed it yourself a little sloppily, you may not have noticed an important difference in how the detailViewController property was declared earlier. Unlike most properties that reference an object pointer, we declared this one using weak instead of strong. This is something that we must do to avoid a retain cycle.
What’s a retain cycle? It’s a situation where a set of two or more objects have references to each other, in a circular fashion. Each object is keeping the memory of the other object from being freed. Most potential retain cycles can be avoided by carefully considering the creation of your objects, often by trying to figure out which object “owns” which. In this sense, an instance of DetailViewController owns an instance of LanguageListController because it’s the DetailViewController that actually creates the LanguageListController to get a piece of work done. Whenever you have a pair of objects that need to refer to one another, you’ll usually want the owner object to retain the other object, while the other object should specifically not retain its owner. Since we’re using the ARC feature that Apple introduced in Xcode 4.2, the compiler does most of the work for us. Instead of paying attention to the details about releasing and retaining objects, all we need to do is declare a property that refers to an object that we do not own with the weak keyword instead of strong. ARC will do the rest!
Now, switch over to LanguageListController.m to implement the following changes. At the top of the file, start by importing the header for DetailViewController:
#import "LanguageListController.h"
#import "DetailViewController.h"
.
.
.
Next, scroll down a bit to the viewDidLoad method and add a bit of setup code:
- (void)viewDidLoad {
[super viewDidLoad];
self.languageNames = @[@"English", @"French", @"German", @"Spanish"];
self.languageCodes = @[@"en", @"fr", @"de", @"es"];
self.clearsSelectionOnViewWillAppear = NO;
self.preferredContentSize = CGSizeMake(320.0,
[self.languageCodes count] * 44.0);
[self.tableView registerClass:[UITableViewCell class]
forCellReuseIdentifier:@"Cell"];
}
This sets up the language arrays and also defines the size that the view controller’s view will use if shown in a popover (which, as we know, it will be). Without defining the size, we would end up with a popover stretching vertically to fill nearly the whole screen, even if it can be displayed in full with a much smaller view. And finally, we register a default table view cell class to use, as explained in Chapter 8.
Further down, we have a few methods generated by Xcode’s template that don’t contain particularly useful code—just a warning and some placeholder text. Let’s replace those with something real:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {#warning Potentially incomplete method implementation.
return 0;
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section {#warning Incomplete method implementation.
// Return the number of rows in the section.
return 0;
return [self.languageCodes count];
}
Now add the tableView:cellForRowAtIndexPath: method to get a cell object and put a language name into a cell:
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell =
[tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];
// Configure the cell...
cell.textLabel.text = self.languageNames[indexPath.row];
return cell;
}
Next, implement tableView:didSelectRowAtIndexPath: so that you can respond to a user’s touch by passing the language selection back to the detail view controller:
- (void)tableView:(UITableView *)tableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
self.detailViewController.languageString =
self.languageCodes[indexPath.row];
}
Note DetailViewController doesn’t actually have a languageString property yet, so you will see a compiler error. We’ll take care of that in just a bit.
Now it’s time to make the changes required for DetailViewController to handle the popover, as well as to generate the correct URL whenever the user either changes the display language or picks a different president. Start by making the following changes in DetailViewController.h:
#import <UIKit/UIKit.h>
@interface DetailViewController : UIViewController
@property (strong, nonatomic) id detailItem;
@property (weak, nonatomic) IBOutlet UILabel *detailDescriptionLabel;
@property (weak, nonatomic) IBOutlet UIWebView *webView;
@property (strong, nonatomic) UIBarButtonItem *languageButton;
@property (strong, nonatomic) UIPopoverController *languagePopoverController;
@property (copy, nonatomic) NSString *languageString;
@end
Here, we added some properties to keep track of the GUI components required for the popover and the user’s selected language. All we need to do now is fix DetailViewController.m so that it can handle the language popover and the URL construction. Start by adding this import somewhere at the top and conforming the class to the UIPopoverControllerDelegate protocol so that it can respond to messages from the UIPopoverController:
#import "DetailViewController.h"
#import "LanguageListController.h"
@interface DetailViewController () <UIPopoverControllerDelegate>
@end
The next thing we’re going to add is a function that takes as arguments a URL pointing to a Wikipedia page and a two-letter language code, and then returns a URL that combines the two. We’ll use this at appropriate spots in our controller code later. You can place this function just about anywhere, including within the class’s implementation. The compiler is smart enough to always treat a function as just a function. Place it just after the setDetailItem: method:
static NSString * modifyUrlForLanguage(NSString *url, NSString *lang) {
if (!lang) {
return url;
}
// We're relying on a particular Wikipedia URL format here. This
// is a bit fragile!
// URL is likehttp://en.wikipedia...
NSRange codeRange = NSMakeRange(7, 2);
if ([[url substringWithRange:codeRange] isEqualToString:lang]) {
return url;
} else {
NSString *newUrl = [url stringByReplacingCharactersInRange:codeRange
withString:lang];
return newUrl;
}
}
Why make this a function instead of a method? There are a couple of reasons. First, instance methods in a class are typically meant to do something involving one or more instance variables, or accessing an object’s internal state either through getters and setters or through direct instance variable access. This function does not use any instance variables. It simply performs an operation on two strings and returns another. We could have made it a class method, but even that feels a bit wrong, since what the method does isn’t really related specifically to the controller class. Sometimes, a function is just what you need.
Our next move is to update the configureView: method. This method will use the function we just defined to combine the URL that’s passed in with the chosen languageString to generate the correct URL:
- (void)configureView {
// Update the user interface for the detail item.
if (self.detailItem) {
NSDictionary *dict = (NSDictionary *)self.detailItem;
NSString *urlString = modifyUrlForLanguage(dict[@"url"], self.languageString);
self.detailDescriptionLabel.text = urlString;
NSURL *url = [NSURL URLWithString:urlString];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[self.webView loadRequest:request];
NSString *name = dict[@"name"];
self.title = name;
}
}
Now let’s update the viewDidLoad method. Here, we’re going to create a UIBarButtonItem and put it into the UINavigationItem at the top of the screen, but only if we are running on an iPad:
- (void)viewDidLoad {
[super viewDidLoad];
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
self.languageButton =
[[UIBarButtonItem alloc] initWithTitle:@"Choose Language"
style:UIBarButtonItemStylePlain
target:self
action:@selector(toggleLanguagePopover)];
self.navigationItem.rightBarButtonItem = self.languageButton;
}
[self configureView];
}
You’ll get a compiler warning here because the code you just added refers to a method called toggleLanguagePopover that doesn’t exist. We’ll fix that soon. Here, we use UI_USER_INTERFACE_IDIOM() to determine whether we’re running on an iPad or an iPhone. We only want to add the button on an iPad, because iPhones do not support UIPopoverController.
Next, we implement setLanguageString:, which is called when the value of the languageString property is changed. This property setter method calls configureView so that the URL can be regenerated (and the new page loaded) immediately, and dismisses the language selection popover if it’s visible. Add this method to the bottom of the file, just above the @end:
- (void)setLanguageString:(NSString *)newString {
if (![newString isEqualToString:self.languageString]) {
_languageString = [newString copy];
[self configureView];
}
if (self.languagePopoverController != nil) {
[self.languagePopoverController dismissPopoverAnimated:YES];
self.languagePopoverController = nil;
}
}
Now, let’s define what will happen when the user taps the Choose Language button. Simply put, we create a LanguageListController, wrap it in a UIPopoverController, and display it. Place this method after the viewDidLoad method:
- (void)toggleLanguagePopover {
if (self.languagePopoverController == nil) {
LanguageListController *languageListController =
[[LanguageListController alloc] init];
languageListController.detailViewController = self;
UIPopoverController *poc =
[[UIPopoverController alloc]
initWithContentViewController:languageListController];
[poc presentPopoverFromBarButtonItem:self.languageButton
permittedArrowDirections:UIPopoverArrowDirectionAny
animated:YES];
self.languagePopoverController = poc;
} else {
[self.languagePopoverController dismissPopoverAnimated:YES];
self.languagePopoverController = nil;
}
}
Finally, we need to implement one more method to handle the situation where the user taps to open our Languages popover, and then taps somewhere outside the popover to make it go away. In that case, our toggleLanguagePopover method isn’t called. However, we can implement a method declared in the UIPopoverControllerDelegate protocol to be notified when that happens, and then remove the language Popovers:
- (void)popoverControllerDidDismissPopovers:
(UIPopoverController *)popoverController {
if (popoverController == self.languagePopoverController) {
self.languagePopoverController = nil;
}
}
And that’s all! You should now be able to run the app in all its glory, switching willy-nilly between presidents and languages. Switching from one language to another should always leave the chosen president intact. Likewise, switching from one president to another should leave the language intact—but actually, it doesn’t. Try this: choose a president, change the language to (say) Spanish, and then choose another president. Unfortunately, the language is no longer Spanish.
Why did this happen? If you go back to “How the Master-Detail Template Application Works” section, you’ll discover the problem—the Show Detail segue creates a new instance of the detail view controller every time it’s performed. That means that the language setting, which is stored as a property of the detail view controller, is going to be lost each time a new president is selected. To fix it, we need to add a few lines of code in the master view controller. Open MasterViewController.m and make the following changes to the prepareForSegue:sender: method:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([[segue identifier] isEqualToString:@"showDetail"]) {
NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
DetailViewController *controller =
(DetailViewController *)[[segue destinationViewController] topViewController];
controller.languageString = self.detailViewController.languageString;
self.detailViewController = controller;
NSDictionary *president = self.presidents[indexPath.row];
controller.detailItem = president;
controller.navigationItem.leftBarButtonItem = self.splitViewController.
displayModeButtonItem;
controller.navigationItem.leftItemsSupplementBackButton = YES;
}
}
Recall that we saved a reference to the detail view controller in the detailViewController property in the master view controller’s viewDidLoad method. Here, when we are about to perform the segue, we use that reference to get the value of the languageString property from the old instance of the detail view controller and copy it to the new instance, which has already replaced the old one in the split view controller’s view hierarchy. Then, we update the detailViewController property of the new instance. That’s all we need to do. Now run the application again. You’ll find that you can switch between presidents without losing your chosen language.
Split Views on the iPhone
As of iOS 8, the split view controller is available on the iPhone as well as the iPad. However, the smaller screen size of the iPhone means that the split view controller works slightly differently than it does on the iPad. Select the iPhone 5s simulator and run the Presidents app in portrait mode. You’ll see the difference immediately (see Figure 11-13): the list of presidents in the master view controller is visible, but the detail view controller’s view is missing. Rotate the device to landscape, and you’ll see that you can still see only the master view controller.
Figure 11-13. The Presidents app running on an iPhone 5s
To activate the detail view controller, just select a president. The detail view controller’s view slides in from the right and the Presidents button appears at the top of the navigation bar, as shown in Figure 11-14. Notice also that the Choose Language button is missing, because we don’t create one when running on an iPhone. If you press the Back button, the detail view controller’s view slides out to the right and the list of presidents reappears.
Figure 11-14. The Presidents app’s detail view controller on iPhone
It’s important to note that we haven’t had to change any code to make the application work on the iPhone. The split view controller sets itself up differently in the constrained screen space of the iPhone, initially showing only the master view controller. In this mode, the split view controller is said to be collapsed. In collapsed mode, the Set Detail segue that we used to link the master view controller to the detail view controller behaves differently—instead of displaying the detail view controller in its own dedicated space on the screen, the split view pushes it onto the view controller stack of the master view controller’s UINavigationController. When you press the Presidents button to redisplay the presidents list, the detail view controller is popped off the stack, exposing the table view controller that was underneath it.
Split Views on the iPhone 6 Plus
The behavior you have just seen applies to all iPhones, with the exception of the iPhone 6 Plus. In landscape mode, the iPhone 6 Plus has a large enough screen to permit the split view to show both view controllers side by side, as it does on the iPad, but only when in landscape mode. Run the application on the iPhone 6 Plus simulator. You’ll initially see just the master view controller, as usual. Now rotate to landscape mode, and you’ll see both view controllers (Figure 11-15).
Figure 11-15. The President’s app in landscape mode on the iPhone 6 Plus
This is similar to, but not exactly the same as, on the iPad. If you compare Figure 11-15 with Figure 11-3, you’ll see that the iPhone version has an extra, double-headed button at the top left of the detail view controller’s navigation bar. This is actually the Presidents button (the one obtained from the displayModeButtonItem property of the UISplitViewController in the application delegate), drawn differently to reflect its modified function. If you press this button, the master view controller is removed, leaving the detail view controller with the whole screen, and the button reverts to its normal appearance. Press the button again to bring the master view controller back into view.
The difference in behavior of the iPhone 6 Plus is another feature that you get for free from UISplitViewController. There are various ways to customize the behavior of the split view, usually by implementing various methods of the UISplitViewDelegate protocol. We’re not going to say anything more about that here, except to point out one detail that you’ll observe if you restart the application and turn the simulator to portrait mode. At this point, as always, you’ll see the master view controller. If you switch between portrait and landscape modes, you’ll continue to see the same controller. Now rotate to landscape mode and select a president, and then rotate back to portrait mode. This time, the detail view controller remains visible—the split view controller did not switch back to the master view controller. This behavior is the result of aUISplitViewDelegate method that’s implemented in AppDelegate.m and is the reason why the application delegate is registered as the split view’s delegate when the application launches. Here’s how that method is implemented:
- (BOOL)splitViewController:(UISplitViewController *)splitViewController
collapseSecondaryViewController:(UIViewController *)secondaryViewController
ontoPrimaryViewController:(UIViewController *)primaryViewController {
if ([secondaryViewController isKindOfClass:[UINavigationController class]]
&& [[(UINavigationController *)secondaryViewController topViewController]
isKindOfClass:[DetailViewController class]]
&& ([(DetailViewController *)
[(UINavigationController *)secondaryViewController topViewController]
detailItem] == nil)) {
// Return YES to indicate that we have handled the collapse by doing nothing;
the secondary controller will be discarded.
return YES;
} else {
return NO;
}
}
This method is called when the split view controller is switching between expanded mode (when both view controllers are active) and collapsed mode (when there is only one). If it returns YES, the detail view controller will be removed; but if it returns NO, the detail view controller will stay in view. The code that the Xcode template produces ensures that the detail view controller is not removed if its detailItem property is not nil—that is, if it is currently displaying something. Interestingly, if you stub out this method so that it always returns NO, you’ll find that the split view controller opens with the detail view controller visible instead of the master view controller.
Getting the iPhone 6 Plus Behavior on All iPhones
It’s possible to get the split view to behave as it does on the iPhone 6 Plus on all iPhones. To see how this is possible, you have to understand why the split view shows both view controllers in landscape mode on the iPhone 6 Plus but not on any other iPhone models. They key to this is a concept that was introduced back in Chapter 5—size classes. If you look at Figure 5-20, you’ll see that the horizontal size class for all iPhones in all orientations is Compact, apart from the iPhone 6 Plus, which has Regular size class in landscape mode. The split view controller operates in collapsed mode in a horizontally compact environment and in expanded mode otherwise. That’s why you can see both view controllers on the iPhone 6 Plus when it’s in landscape orientation. As it turns out, you can use this fact to get the same behavior on all iPhones. All you have to do is convince the split view controller that its horizontal size class is Regular.
Size class information is propagated to a view controller from its parent view controller, if it has one, or from its window if it’s the top-level view controller. The size class information is delivered as part of a trait collection, represented by the UITraitCollection class, when the view controller’s view is displayed and when the device is rotated, if that would cause either its horizontal or vertical size class to change. All view controllers conform to the UITraitEnvironment protocol, which means that they have a traitCollection property that holds the current set of traits and they implement the following method, which is called after the value of the traitCollection property has been changed:
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
To make the split view controller believe that it has a Regular horizontal size class, we need to change the trait collection that’s passed to it by its parent view controller. That brings up a problem: in the storyboard that’s created by the Master-Detail application template (see Figure 11-6), the split view controller is the root view controller of its window, so it doesn’t have a parent view controller. To change its trait collection, we’ll first have to give it a parent view controller, so let’s do that.
Select Main.storyboard, open the Object Library, and drag a UINavigationController onto the storyboard. We’re going to make this controller the parent of the split view controller. It already has a UITableViewController child, which we don’t need, so select it (it’s the one labeled Root View Controller in the Document Outline) and delete it. Next, Control-drag from the navigation controller to the split view controller, and then release the mouse. In the pop-up, select Root View Controller from the Relationship Segue section to make the navigation controller the parent of the UISplitViewController. There are two final steps to complete. Select the navigation controller and open the Attributes Inspector. In the Navigation Controller section, uncheck Shows Navigation Bar (since we don’t need to be able to navigate) and in the View Controller section, check Is Initial View Controller to make the navigation controller the root view controller of the application window.
Note If you are wondering why we used a UINavigationController as the root view controller instead of a plain UIViewController, the reason is that Interface Builder won’t let you drag to create a controller connection from an ordinary UIViewController, because there is no rootViewController property. You could create the connection in code (as we did in Chapter 6), but it’s easier to just use a UINavigationController and switch off the navigation bar.
At this point, your storyboard should look something like Figure 11-16.
Figure 11-16. The storyboard of the Presidents app with a new root view controller
Now that we’ve given the split view a parent view controller, we need to override its traitCollectionDidChange: method. To do that, we need to substitute our own UINavigationController subclass for the one in the storyboard. Right-click the Presidents folder in the Project Navigator and select New File. . ., and then choose Cocoa Touch Class from the iOS Source section of the new file chooser and click Next. Give the new class the name RootViewController, make it a subclass of UINavigationController, and create it. Select the navigation controller in Main.storyboard, open the Identity Inspector, and set its Class to RootViewController.
So now our navigation controller subclass is the window’s root view controller and we are almost ready to override its traitCollectionDidChange: method, but we have one more thing to fix before we do that. The template-generated code inapplication:didFinishLaunchingWithOptions: in AppDelegate.m assumes that the split view controller is the root view controller. Since that’s no longer the case, we have to make a small change. Open AppDelegate.m and make the following changes shown in bold:
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
UISplitViewController *splitViewController =
(UISplitViewController *)self.window.rootViewController;
UINavigationController *rootViewController =
(UINavigationController *)self.window.rootViewController;
UISplitViewController *splitViewController =
(UISplitViewController *)rootViewController.viewControllers[0];
UINavigationController *navigationController = [splitViewController.viewControllers lastObject];
navigationController.topViewController.navigationItem.leftBarButtonItem =
splitViewController.displayModeButtonItem;
splitViewController.delegate = self;
return YES;
}
Now let’s do what we set out to do. Open RootViewController.m in the editor and add the following code to it:
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
UIViewController *spltVC = self.viewControllers[0];
UITraitCollection *newTraits = self.traitCollection;
if (newTraits.horizontalSizeClass == UIUserInterfaceSizeClassCompact
&& newTraits.verticalSizeClass == UIUserInterfaceSizeClassCompact) {
UITraitCollection *childTraits = [UITraitCollection
traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassRegular];
[self setOverrideTraitCollection:childTraits forChildViewController:spltVC];
} else {
[self setOverrideTraitCollection:nil forChildViewController:spltVC];
}
[super traitCollectionDidChange:previousTraitCollection];
}
The first thing we do is get the newly installed set of traits from the root view controller’s traitCollection property. If both the horizontal and vertical size classes are Compact, then we must be running on an iPhone that’s been rotated to landscape. This is the case in which we need to change the horizontal size class that the split view will see from Compact to Regular. We do that by creating a trait for the regular size class using a class method of UITraitCollection:
UITraitCollection *childTraits = [UITraitCollection
traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassRegular];
Next, we tell the root view controller to override the traits of its split view controller child with this new trait:
[self setOverrideTraitCollection:childTraits forChildViewController:spltVC];
On the other hand, if we have any other combination of size classes, we don’t need to change them, so we install a nil override:
[self setOverrideTraitCollection:nil forChildViewController:spltVC];
Now build and run the application, and then run it on any iPhone simulator. Rotate to landscape, and you’ll see both view controllers, just like you would on the iPhone 6 Plus.
Incidentally, you can even force the split view controller to show both view controllers in portrait mode by modifying the traitCollectionDidChange: method so that it always installs an override trait. It’s worth trying that just to see that it works, but the screen is too narrow for this to be useful in most cases.
Customizing the Split View
There are a couple of split view controller customizations available that are worth experimenting with. These work on any device. First, you can control the width of the area allocated to the master view controller when both view controllers are visible. To do this, you need to set the split view controller’s preferredPrimaryColumnWidthFraction and maximumPrimaryColumnWidth properties. The former sets the width of the master view controller as a fraction of the total space available and requires a value between 0 and 1. The latter acts as an upper bound on its width, so you need to set this property if you need the master view controller to be wider than the default value calculated by the split view controller.
To see how this works, make the following changes to application:didFinishLaunchingWithOptions: in AppDelegate.m:
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
UINavigationController *rootViewController =
(UINavigationController *)self.window.rootViewController;
UISplitViewController *splitViewController =
(UISplitViewController *)rootViewController.viewControllers[0];
UINavigationController *navigationController =
[splitViewController.viewControllers lastObject];
navigationController.topViewController.navigationItem.leftBarButtonItem =
splitViewController.displayModeButtonItem;
splitViewController.delegate = self;
splitViewController.preferredPrimaryColumnWidthFraction = 0.5;
splitViewController.maximumPrimaryColumnWidth = 600;
return YES;
}
Run the application again on any simulator and rotate to landscape mode. You’ll see that the master view controller now occupies half of the screen (see Figure 11-17), because we set preferredPrimaryColumnWidthFraction to 0.5 and increasedmaximumPrimaryColumnWidth to a value that’s large enough that it doesn’t limit the master view controller’s width on any current device.
Figure 11-17. Increasing the width of the master view controller in a split view
The second customization controls the way in which the master view controller is managed. By default, the split view controller determines when this controller is visible and how it appears and disappears. For example, on the iPad in portrait mode, the master view controller is initially invisible and slides in from the left of the screen; whereas in landscape mode, it’s initially visible and cannot be hidden. This behavior is controlled by the split view controller’s preferredDisplayMode property. By default, this is set toUISplitViewControllerDisplayModeAutomatic, but there are three other choices available:
· UISplitViewControllerDisplayModePrimaryOverlay: Places the master view controller on the left, overlaying the detail view controller. When the master view controller is dismissed, it slides away to the left.
· UISplitViewControllerDisplayModePrimaryHidden: The same as UISplitViewControllerDisplayModePrimaryOverlay, except that the master view controller is initially hidden.
· UISplitViewControllerDisplayModeAllVisible: Makes both view controllers initially visible on the screen.
The actual behavior depends on the type of device. For example, in horizontal Compact mode, this property has no effect, since both view controllers are never on the screen at the same time.
You can try out each of these modes by setting the preferredDisplayMode property in the application:didFinishLaunchingWithOptions: method. For example:
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
UINavigationController *rootViewController =
(UINavigationController *)self.window.rootViewController;
UISplitViewController *splitViewController =
(UISplitViewController *)rootViewController.viewControllers[0];
UINavigationController *navigationController =
[splitViewController.viewControllers lastObject];
navigationController.topViewController.navigationItem.leftBarButtonItem =
splitViewController.displayModeButtonItem;
splitViewController.delegate = self;
splitViewController.preferredPrimaryColumnWidthFraction = 0.5;
splitViewController.maximumPrimaryColumnWidth = 600;
splitViewController.preferredDisplayMode = UISplitViewControllerDisplayModePrimaryOverlay;
return YES;
}
Time to Wrap Up and Split
In this chapter, you learned about the split view controller and its role in the creation of Master-Detail applications. You also saw that a complex application with several interconnected view controllers can be configured entirely within Interface Builder. Although split views are now available on all devices, they are probably still most useful in the larger screen space of the iPhone 6 Plus and the iPad. If you want to dig even further into the particulars of iPad development, you may want to take a look at Beginning iPad Development for iPhone Developers by David Mark, Jack Nutting, and Dave Wooldridge (Apress, 2010).
Next up, it’s time to visit application settings and user defaults.