iOS App Development For Dummies (2014)

Part IV. The Model and the App Structure

Chapter 13. Working with Split View Controllers and the Master View

In This Chapter

arrow Working with Split View controllers

arrow Checking out the versatile Table view

arrow Making the table look good

arrow Ensuring that your app is usable worldwide

arrow Peeking behind the Table View screen

arrow Using model objects

This chapter is primarily about the iPad user interface because Split View controllers aren’t supported on the smaller iPhone. In Chapter 5, I explain a bit about iPad navigation using the example of a Navigation controller within a Split View controller. But the iPad has some even slicker ways to navigate, which I get to in this chapter. You’ll be happy to know that you’re going to be replacing the Navigation controller style of navigation with something more appropriate to the RoadTrip app.

The Problem with Using a Navigation Controller in Detail View

As I explain in Chapter 5, the Master-Detail Application template you’re using sets up the iPad’s Split View controller in such a way that, when a cell in the Master view is tapped (Test Drive, for example), the Navigation controller pushes the next view controller onto the stack. The new view controller’s view slides into place, and the Navigation bar items are updated appropriately. The result, as you can see on the right side of Figure 13-1, is a Back button (labeled Detail) in the Detail view that enables you to return to the previous Detail view. (I’m using the pre–Chapter12 version of RoadTrip because it is easier to show you the flow in Figure 13-1.)

image

Figure 13-1: On the iPad, it's back and forth using a Navigation controller and Navigation bar.

Although the user experiences on the iPhone and iPad have similarities, in some ways, the experiences are quite different. And one major and very obvious difference is the screen size — a difference that can have a major impact on the ways you can navigate and display information.

On the iPhone, you go from one view to another using the navigator approach you see in Figure 13-2.

image

Figure 13-2: Navigating with a Navigation controller on the iPhone.

The only difference is that, with the increased real estate on the iPad, you can show both the Table view that you use for navigation as well as the associated content you want to display at the same time, as you see in Figure 13-3.

But while you can take advantage of the larger display using the Navigation controller approach, a problem is lurking in the background that I want to call your attention to.

The process doesn’t initially appear to be particularly problematic if you stick to Landscape orientation, but when you switch to Portrait orientation, you soon realize that you're in a bit of a bind. You can see the outlines of that bind in the sequence in Figure 13-4. Again, in the current (unimproved) version of RoadTrip, when you're in Portrait orientation, tapping the Master button displays the Master view in a popover. If you then tap the Test Drive cell, the Test Drive view slides into place. (You also need to tap anywhere in the Test Drive view to dismiss the popover — you’ll fix that so it's automatically dismissed when a new view is displayed.)

image

Figure 13-3: Master view and Detail view using a Split view.

image

Figure 13-4: Navigating in a Split view using a Navigation controller.

At that point, you see a Back button (Detail) that’s designed to get you back to the original view. This is the essence of a navigation interface: You move through the sequence of views drilling deeper and deeper into details and then stepping back up until you’re at the starting point.

When you are using a Split View controller on the iPad, you can keep the top level of choices visible in the Master view controller at all times. Typically, you leave that view in place and vary the views shown in the detail view in response to choices made in the Master view controller. This gives you two ways to traverse the data. You can use the navigation controller in the Detail view to drill down (and then back up), and you can use the Master View controller to switch from one top-level view to another.

Using a Navigation Controller in the Master View

You might want to use a Navigation controller in the Master view. I’ll show you how to do that by creating a segue — a storyboard object that creates a transition from one view controller to another — so that when you select Test Drive in the Master view, the Test Drive view will slide into place, Back button and all, in the Master view (as opposed to the Detail view, which had been the case). Then, when you tap the Back button, you’ll be back in the Master view.

The technique for replacing a view controller with another is the same whether you’re working in the Master or the Detail view, and that’s what I’m showing you in this section — replacing the Detail view controller. After I explain it, I’ll have you go back to having the Test Drive view display in the Detail view.

Here is how to create a push segue that allows your Test Drive button to display the Test Drive controller in the Master view.

1.     Select Main_iPad.storyboard in the Project navigator.

The storyboard will appear.

2.     Select the Test Drive cell, control-drag it to the Test Drive controller, as I have done in Figure 13-5, and then release the mouse button.

You’ll see the pop-up menu that allows you to select the Storyboard Segues type, as shown in Figure 13-6.

image

Figure 13-5: Control-drag to create a segue.

image

Figure 13-6: Create a push segue.

3.     Choose Push from the Selection Segue menu items.

Let me remind you that I explained the difference between Selection Segues and Accessory Actions in Chapter 5. You’ll notice that the Test Drive view resizes to the same size as the Master view.

Select the segue in the Document Outline, as I have done in Figure 13-7. The Attributes inspector shows you that the Destination is set to Current. (Current is in fact the default.) That means that the Destination view for this segue — the view that is displayed by the Test Drive controller — is the view that the segue is from, or the Master view.

image

Figure 13-7: The Destination is Current — the Master View.

4.     Enter TestDrive in the Identifier field, as I have in Figure 13-7.

5.     Select the Table View cell and, in the Attributes inspector, change the Accessory field from Disclosure Indicator to None.

I’m of the opinion that a disclosure indicator isn’t necessary here, but feel free to reject my opinion if you so desire.

If you click the Run button now and then tap Test Drive in the Master view, you get to test drive the 1959 Cadillac Eldorado Biarritz in the Master view; tapping the New York City button (as you recall, what appears in the Back button is the title of the previous view controller, and you made that your destination in Chapter 12) in the Test Drive view takes you back to the Master view. This works even in Portrait orientation, as you can see in Figure 13-8.

image

Figure 13-8: Using a Navigation controller in the Master view.

If you tap Road Trip to return to the Master view, you may notice that the Test Drive cell stays highlighted; not to worry, you’ll fix that soon.

Adding a Gesture Recognizer

If you want to truly understand the iOS user experience, you need to understand the importance of gestures, as I explain in Chapter 1.

UIKit includes gesture recognizers that you can use in your app. In this section, you’ll use UIKit to add a gesture recognizer to the Main view so that the user can swipe to the left to make the Test Drive view appear.

Adding the gesture recognizer is easy for both the iPhone and iPad. In MasterViewController.m, add the bolded code in Listing 13-1 to viewDidLoad.

Listing 13-1: Adding a Gesture Recognizer

  - (void)viewDidLoad
{
  [super viewDidLoad];
  AppDelegate* appDelegate = 
            [[UIApplication sharedApplication] delegate];
  self.title = appDelegate.trip.destinationName;
  UIImageView* imageView = [[UIImageView alloc] 
       initWithImage:[appDelegate.trip destinationImage]];
  self.tableView.backgroundView = imageView;

_detailViewController = (DetailViewController *)
  [[self.splitViewController.viewControllers 
  lastObject] topViewController];
  
  UISwipeGestureRecognizer *swipeGesture = 
  [[UISwipeGestureRecognizer alloc] initWithTarget:self  
                  action:@selector(handleSwipeGesture:)];
  swipeGesture.direction =       
                  UISwipeGestureRecognizerDirectionLeft;
  [self.view addGestureRecognizer:swipeGesture];
}

UISwipeGestureRecognizer is a subclass of UIGestureRecognizer — the abstract base class for concrete gesture-recognizer classes. The gesture recognizer does the hard work of recognizing a specific gesture and then sends an action message (that you specify) to the target (that you also specify) to go ahead and do something.

In addition to UISwipeGesture, you have gesture recognizers for

·        Tap:UITapGestureRecognizer

·        Pinch:UIPinchGestureRecognizer

·        Rotate:UIRotationGestureRecognizer

·        Pan:UIPanGestureRecognizer

·        Touch and hold:UILongPressGestureRecognizer

A window delivers touch events to a gesture recognizer before it delivers them to the hit-tested view — the view where it determined the user has touched — attached to the gesture recognizer. (Note that the gesture recognizer is attached to the view and isn't part of the responder chain.) Generally, if a gesture recognizer doesn't recognize its gesture, the touches are passed on to the view. If a gesture recognizer does recognize its gesture, the remaining touches for the view are canceled.

UISwipeGestureRecognizer is a concrete subclass of UIGesture
Recognizer that looks for swiping gestures in one or more directions. Because a swipe is a discrete gesture, the action message is sent only once per gesture.

UISwipeGestureRecognizer recognizes a gesture as a swipe when the specified number of touches (numberOfTouchesRequired) have moved mostly in an allowable direction (direction) far enough to be considered a swipe. You can configure the UISwipeGestureRecognizer recognizer for the number of touches (the default is 1) and the direction (the default is right), as follows:

  UISwipeGestureRecognizer *swipeGesture = 
  [[UISwipeGestureRecognizer alloc] initWithTarget:self 
                   action:@selector(handleSwipeGesture:)];

Here, you create a swipe gesture with a target of self and an action of handleSwipeGesture:. This means that when the gesture recognizer determines it is a swipe, it will send the handleSwipeGesture: to the MasterViewController (self in this case).

Next, because we want the user to swipe to the left to make the Test Drive view appear, you set the direction to left from the default right, as follows:

  swipeGesture.direction =         
           UISwipeGestureRecognizerDirectionLeft;

To handle the swipe — in effect program the response you want to come up with when the swipe occurs — add the code in Listing 13-2 to MasterViewController.m.

Listing 13-2: Adding handleSwipeGesture to MasterViewController.m

   - (IBAction)handleSwipeGesture:(id)sender {
  
  UIStoryboard *storyboard = 
  [UIStoryboard storyboardWithName:@"Main_iPad" 
          bundle:nil];
  UIViewController *viewController = 
   [storyboard instantiateViewControllerWithIdentifier:
          @"TestDrive"];  
  [[self navigationController] 
          pushViewController:viewController 
animated:YES];
}

What you do here is first find the storyboard in the bundle — in iOS (and OS X), a bundle is a directory that appears to be a single file and contains the executable code, resources such as images and sound, and the nib (storyboard) files.

  UIStoryboard *storyboard = 
  [UIStoryboard storyboardWithName:
                           @"Main_iPad" 
           bundle:nil];

Then the code in Listing 13-2 creates TestDriveController. This is the same thing that the storyboard does (in the segue logic) when you tap the Test Drive button:

  UIViewController *viewController = 
  [storyboard instantiateViewControllerWithIdentifier:
           @"TestDrive"];

Here’s where that identifier I told you to enter is needed. It’s the only way you can find the view controller that you've configured in the storyboard.

Next, you tell the Navigation controller to push the View controller onto the stack (note that this method also updates the Navigation bar) and have it slide its view into place. (If the animated parameter is YES, the view is animated into position; otherwise, the view is simply displayed in place.)

  [[self navigationController] pushViewController:
                            viewController animated:YES];

This is what would've been done for you in the segue logic generated by the storyboard.

image Oh, and by the way — you’ve only installed the Swipe gesture in the Master View controller. When you are in the Test Drive view, you can only go back by using the Back (New York City) button. In designing an interface, consistency and symmetry are good features to strive for. If you swipe into a view, maybe you should swipe out of a view. That’s how you build powerful, intuitive, and easy-to-learn interfaces. Unfortunately, that’s not a great way to write a book. I want to show you as many different ways of working with Cocoa Touch as possible. Sometimes that means swiping into a new view and getting back out of it with a button instead of a matching swipe. Many developers like to show off all of the features and interface elements they’ve added to their apps. Many others (including the authors of some of the best apps) like to show off how few features and interface elements they’ve used to build a wide swath of functionality.

So, rather than push view controllers here, there, and everywhere, in the next few sections, you find out how to replace one view controller with another. And the place to start is with the Split View controller.

The Split View Controller

The UISplitViewController class is an iPad-only view controller that simply manages the presentation of two side-by-side view controllers in Landscape orientation — it is a container controller. It has no interface — its job is to coordinate the presentation of its two view controllers and to manage the transitions between different orientations.

Using this class, a view controller is created on the left, which is referred to as the Master view (and is named that in the template), which presents a list of items, and another view controller on the right, which presents the details, or content, of the selected item and which is referred to as the Detail view (and is named that in the template).

After you create and initialize a UISplitViewController object, you assign two view controllers to it by using the viewControllers property — an array that contains the two (and only the two) controllers. The first view controller (index 0) is displayed on the left side (in the case of RoadTrip, the MasterViewController), and the second view controller (index 1) is displayed on the right side (the RTDetailViewController).

All of this is set up for you in the storyboard (see Figure 13-9). You’ll notice that both of the view controllers managed by the Split View controllers are embedded in Navigation controllers, which is why when you select a cell in the MasterViewController view, a Navigation bar appears with a Back button (in either the Master or Detail view).

image

Figure 13-9: The Master and Detail View controllers in the storyboard.

As you have seen, when the iPad is in Landscape orientation, you can see both view controllers’ views side by side. When you rotate to Portrait orientation, however, the MasterViewController’s view is hidden, and the DetailViewController displays a button in the Navigation bar that enables you to see the MasterViewController’s view in a popover. Adding the button is done in the UISplitViewControllerDelegate protocol method of the template (you don’t have to do anything to implement it). And if you look at the DetailViewController interface file, you see it has adopted the protocol — the popover sends its delegate messages at the right time so that the delegate can add and remove the button.

The user interface I want to show you now uses a toolbar instead of a Navigation bar, and has the Detail View controller be responsible for managing the toolbar. In fact, I’ll show you how to implement the delegate to use either a toolbar or a Navigation bar when you choose a cell in theMasterViewController. I’m doing both because some of the features I want to implement (like the UIPageViewController page transitions in the EventsController) require a Navigation bar. In addition, at some point you may want to implement some functionality where a Navigation controller is the right way to go. Remember my caution about using a limited number of interface tools. This mixture of Navigation bar and toolbar is designed primarily to show you different ways of designing the interface; you may well decide to choose one rather than mix them together to possibly befuddle your users. On the other hand, remember that if your user interface makes sense to the user, the fact that behind the scenes you’re using two different types of objects doesn’t matter.

To start with, you’ll add a toolbar to the TestDriveController and make TestDriveController a UISplitViewController delegate. But first I want to explain a little more about how the UISplitViewController delegate works.

The UISplitViewController delegate

The UISplitViewController does what you’d expect a class doing delegation to do: It sends its delegate messages at the appropriate times to add and remove the button that enables the user to display the Master View controller in the popover you see in Figure 13-10. The Master button is displayed in Portrait view until the user touches it. Then it is covered by the Master view, which slides in from the left.

image

Figure 13-10: The Road Trip button that displays the Master view in Portrait orientation.

If you look at the DetailViewController interface (DetailViewController.h) in your project, you can see that this whole delegation business has already been done for you. Listing 13-3 gives the details.

Listing 13-3: The RTDetailViewController Interface

  #import <UIKit/UIKit.h>

@interface RTDetailViewController : UIViewController  
           <UISplitViewControllerDelegate>

@property (strong, nonatomic) id detailItem;
@property (strong, nonatomic) 
                 IBOutlet UILabel 
*detailDescriptionLabel;

@end

You see that DetailViewController adopts the UISplitViewController
Delegate protocol, and as you see in Figure 13-10, it already adds a button to the Navigation bar when the user moves into Portrait orientation. It also removes the button when the iPad is rotated back from Portrait to Landscape orientation.

All you’ll need to do is modify what the UISplitViewControllerDelegate already does and have it place the button in a toolbar or a Navigation bar, depending on which you’re using. You’ll do all of that in the DetailViewController, and then, if you end up deriving all your view controllers (including the TestDrive
Controller you’ve already implemented) from DetailViewController, voilà, all your view controllers will be able to add a button to the toolbar or a Navigation bar when the iPad is in Landscape orientation, dutifully display the popover when the user selects the button, and then remove the button when the user rotates the iPad to Portrait orientation.

To prepare for that occurrence, you’ll start by getting rid of the “sample” code that was included by the template to display something in the Detail view — you won’t use the Detail View controller to actually display anything; that will be done in the individual view controllers you'll create to display the Road Trip content (the weather, for example, or a map) as well as the Test Drive controller you’ve already created.

Start by pruning two properties used by the Master View controller that you definitely won’t need. Delete the commented out bold, underline, and italic lines in Listing 13-4 in DetailViewController.h, and add the code in bold. (You’ll add some properties that will be used in managing the popover.)

Listing 13-4: Update DetailViewController.h Interface

  #import <UIKit/UIKit.h>

@interface DetailViewController : UIViewController 
           <UISplitViewControllerDelegate>

//@property (strong, nonatomic) id detailItem;
//@property (strong, nonatomic) 
             IBOutlet UILabel
        *detailDescriptionLabel;

@property (weak, nonatomic) IBOutlet UIToolbar *toolbar;
@property (weak, nonatomic) 
                          UIBarButtonItem * popOverButton; 
@property (strong, nonatomic) 
             UIPopoverController *masterPopoverController;
@end

I’ll explain these new properties as you use them, but it's worth noting here that you’re adding a toolbar as a home for the button the user will use to display the Master View controller in Portrait orientation.

You have more things to prune in the DetailViewController implementation file. To do that, delete the commented out code in bold, underline, and italic in Listing 13-5 from the DetailViewController.m implementation file.

Listing 13-5: Update the DetailViewController.m Implementation File

  #import "DetailViewController.h"


@implementation DetailViewController

Here you’re mostly getting rid of the class extension and instance variables you no longer need. In Listing 13-4, you also moved the masterPopoverController property to the interface file from where it was to the implementation file. As you’ll soon see, you had to do that because the Master View controller will need to access the masterPopoverController property in order to dismiss the popover after the user makes a selection in the Master View controller in the popover.

Finally you can delete the methods that display “content” in the Detail view — that means the two methods in bold, underline, and italic in Listing 13-6 in DetailViewController.m.

Listing 13-6: Delete the Unnecessary Methods

  #pragma mark - Managing the detail item

- (void)setDetailItem:(id)newDetailItem
{
    if (_detailItem != newDetailItem) {
        _detailItem = newDetailItem;

         // Update the view.
        [self configureView];
    }

    if (self.masterPopoverController != nil) {
      [self.masterPopoverController 
                             dismissPopoverAnimated:YES];
    }       
}

- (void)configureView
{
    // Update the user interface for the detail item.

  if (self.detailItem) {
      self.detailDescriptionLabel.text = 
                           [self.detailItem description];
  }
}

You’ll need to delete the message sent to configure the view. In DetailView
Controller.m, delete the commented out code in bold, underline, and italic in viewDidLoad, as shown in Listing 13-7.

Listing 13-7: Delete the Unnecessary Code in viewDidLoad

  - (void)viewDidLoad
{
    [super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
//  [self configureView];
}

Now, I look at how the delegate mechanism works and how the button is added when the iPad is rotated from Landscape to Portrait orientation and then removed when it is rotated back. In DetailViewController.m, the code in this template already does what you need to do to display the kind of popover I just explained. It does so by implementing two UISplitViewController delegate methods:

  splitViewController:willHideViewController:
           withBarButtonItem:forPopoverController:

and

  splitViewController:willShowViewController:
           invalidatingBarButtonItem:

The first of these methods is invoked when the Split View controller rotates from a Landscape to Portrait orientation and hides the Master View controller. When that happens, the Split View controller sends a message to add a button to the Navigation bar (or toolbar) of the Detail View controller. If you look in Listing 13-8, this is how it works in DetailViewController.m:

Listing 13-8: Adding the Button

  - (void)splitViewController:
                 (UISplitViewController *)splitController
       willHideViewController:
                        (UIViewController 
*)viewController 
       withBarButtonItem:
                          (UIBarButtonItem 
*)barButtonItem 
       forPopoverController:
                 (UIPopoverController *)popoverController
{
    barButtonItem.title = 
                 NSLocalizedString(@"Master", @"Master");
    [self.navigationItem setLeftBarButtonItem:
                            barButtonItem animated:YES];
    self.masterPopoverController = popoverController;
}

In this method, you are passed a button that you can use to display the popover. This is no ordinary button, however; its target is a selector implemented for you that can display the popover. The way the app works now, only one view controller displays a popover — but in the final version of the RoadTrip app, all the view controllers will be able to display a popover, so you’ll need to pass that button to the view controller that's replacing the current one.

You’ll notice the presence of NSLocalizedString in Listing 13-8. NSLocalized
String is a macro that can be used to localize the text you display. And because I let the term localize drop here, I might as well explain about localization.

Localization

When you localize an app, you display the text the user sees on the screen in the user’s preferred language (and even a language-specific image).

Localizing an app isn’t difficult, just tedious. To localize your app, you create a folder in your application bundle (I’ll get to that) for each language you want to support. Each folder has the app’s translated resources.

For example, if the user’s language is Spanish, available regions range from Spain to Argentina to the United States and lots of places in between. When a localized app needs to load a resource (such as an image, property list, or nib), the app checks the user’s language and region and looks for a localization folder that corresponds to the selected language and region. If it finds one, it loads the localized version of the resource instead of the base version — the one you’re working in.

Looking up such values in the table is handled by the NSLocalizedString macro in your code.

As you can see, the text Master button will change based on the user’s preferred language.

  barButtonItem.title = 
                 NSLocalizedString(@"Master", @"Master");

The macro has two inputs. The first key to an associated string value, and the second is the general comment for the translator. At runtime, NSLocalizedString looks for a strings file named localizable.strings in the language that has been set: Spanish, for example. (A user would've done that by going to Settings and selecting General⇒International⇒Language⇒Español.) If NSLocalizedString finds the strings file, it searches the file for a line that matches the first parameter. If the macro doesn’t find the file or a specified string, it returns its first parameter — and the string will appear in the base language.

To create the localizable.strings file, you run a command-line program named genstrings, which searches your code files for the macro and places them all in a localizable.strings file (which it creates), ready for the (human) translator. genstrings is beyond the scope of this book, but it’s well documented. When you’re ready, I leave you to explore it on your own.

Back to the main feature

After absorbing that little localization detour, you then add the longed-for button to the Navigation bar:

   [self.navigationItem setLeftBarButtonItem:
                            barButtonItem animated:YES];

Passing in YES to animated animates the adding of the button. Specifying NO sets the item immediately, without animating the change. In this case, it really makes no difference.

Finally, you save a reference to the popover controller that you'll dismiss when the user makes a selection:

      self.masterPopoverController = popoverController;

As I said, when the user selects a new view in the popover, you’ll need to pass this button on to the new view controller so it can display it as well. (You’ll do that later in this chapter in a method called prepareForSegue: in Listing 13-15.) In addition, as I explained, you’ll also use toolbars in addition to Navigation bars to display the button; adding the button to a toolbar is done differently than adding it to the Navigation bar. To do that, enter the code in bold in Listing 13-9 and delete the bold, underline, and italic code in DetailViewController.m.

Listing 13-9: Extending the splitViewController: willHideViewController: withBarButtonItem:forPopoverController:

  - (void)splitViewController:
                 (UISplitViewController *)splitController 
      willHideViewController:
                        (UIViewController
       *)viewController 
      withBarButtonItem:
                          (UIBarButtonItem
       *)barButtonItem 
      forPopoverController:
                  (UIPopoverController
       *)popoverController
{
//  barButtonItem.title = NSLocalizedString(@"Master",
       @"Master");
  barButtonItem.title = 
         NSLocalizedString(@"Road Trip", @"Road Trip");
   if ([[self.splitViewController.viewControllers lastObject] isKindOfClass:[UINavigationController class]]) 
    [self.navigationItem 
         setLeftBarButtonItem:barButtonItem 
animated:YES];
  else {
    NSMutableArray *itemsArray = 
                         [self.toolbar.items mutableCopy];
    [itemsArray insertObject:barButtonItem atIndex:0];
    [self.toolbar setItems:itemsArray animated:YES];
  } 
  self.masterPopoverController = popoverController;
  self.popOverButton = barButtonItem;
}

Listing 13-9 starts off by changing the title of the button from Master to Road Trip — I think it’s less ominous-sounding and more descriptive — indicating the view controller that you’d see if you tap the button:

  //barButtonItem.title = 
                 NSLocalizedString(@"Master", @"Master");
  barButtonItem.title =
            NSLocalizedString(@"Road trip", @"Road 
Trip");

You then check to see whether you’re dealing with a Navigation bar or toolbar.

    if ([[self.splitViewController.viewControllers lastObject] 
isKindOfClass:[UINavigationController class]])

As I mention earlier, the Split View controller manages two view controllers, with the last one in its list of controllers corresponding to what's displayed in the Detail view. You check if the Detail View controller is embedded in a Navigation controller by sending it the isKindOfClass:message. This method returns a Boolean indicating whether it is, in this case, a UINavigationController.

As you know, classes in Objective-C are first-class objects in and of themselves, so you can use a class method to determine what class something is. This method is defined in the NSObject class, from which all your classes are (ultimately) derived.

If you’re dealing with a Navigation controller, you do what you’ve already been doing and set the left bar button.

  [self.navigationItem 
         setLeftBarButtonItem:barButtonItem 
animated:YES];

If you’re not dealing with a Navigation controller, you go through some similar logic to add the button to the toolbar.

  NSMutableArray *itemsArray = 
                        [self.toolbar.items mutableCopy];
[itemsArray insertObject:barButtonItem atIndex:0]; 
[self.toolbar setItems:itemsArray animated:NO];

On a toolbar, the buttons are specified in the items property as an array. So you make a copy of the array and add the button you're passed to the top of the array. The items — instances of UIBarButtonItem — are shown on the toolbar in the order they appear in this array. You then take that array and assign it as the items property.

You’ll notice that you first make a copy of the toolbar items, insert the button, and then assign the items property. You do it this way because there may be other buttons on the toolbar that you’ll want to keep there.

Then you save a reference to the button that you’ll use later.

    self.popOverButton = barButtonItem;

The second of the delegate methods is invoked when the view controller rotates from Portrait to Landscape orientation and the “hidden” Master View controller is displayed.

When the iPad is rotated back to Landscape orientation, the splitViewController:willShowViewController:invalidatingBarButtonItem: message is sent.

If you added the button to your toolbar to allow the user to display the Master View controller in a popover, you remove the button in the splitViewController:willShowViewController:invalidatingBarButtonItem: method. Listing 13-10 shows howsplitViewController:willShowViewController:invalidatingBarButtonItem: is currently implemented in DetailViewController.m:.

Listing 13-10: splitViewController:willShowViewController: invalidatingBarButtonItem:

  - (void)splitViewController:
                 (UISplitViewController *)splitController willShowViewController:
                       (UIViewController *)viewController invalidatingBarButtonItem:
                         (UIBarButtonItem *)barButtonItem
{
// Called when the view is shown again in the split 
view, invalidating the button and popover controller.
    [self.navigationItem 
                setLeftBarButtonItem:nil animated:YES];
    self.masterPopoverController = nil;
}

This method as implemented sets the leftBarButtonItem and the master
PopoverController (the reference to the popover controller) to nil.

Here you’ve simply reversed what you did earlier — you’ve removed the button from the toolbar and set the self.popoverController property to nil.

As with the splitViewController:willHideViewController:withBarButtonItem: forPopoverController: method, you’ll now have to deal with both a toolbar and a Navigation controller. So add the code in bold in Listing 13-11 tosplitViewController:willShowViewController:invalidatingBarButtonItem: in DetailViewController.m.

Listing 13-11: Update splitViewController:willShowViewController: invalidatingBarButtonItem:

  - (void)splitViewController:
                 (UISplitViewController *)splitController willShowViewController:
                       (UIViewController *)viewController invalidatingBarButtonItem:
                         (UIBarButtonItem *)barButtonItem
{
  if 
   ([[self.splitViewController.viewControllers lastObject]   
            isKindOfClass:[UINavigationController class]]) 
    [self.navigationItem setLeftBarButtonItem:nil 
           animated:YES];
  else {
    NSMutableArray *itemsArray = 
                         [self.toolbar.items mutableCopy];
    [itemsArray removeObject:barButtonItem];
    [self.toolbar setItems:itemsArray animated:YES];
  }
  self.popOverButton = nil;
  self.masterPopoverController = nil;
}

The logic here determines whether it's a Navigation controller or a toolbar, with managing the toolbar identical to what you did in Listing 13-9, when you extended the splitViewController:willHideViewController: withBarButtonItem:forPopoverController: method.

While you have the button saved, you still need to transfer the button — and have it displayed — when the user taps the Road Trip button and selects an entry (Weather, for example) that results in a new view controller being instantiated to display its view.

The display part will be done in the viewDidLoad method of the new controller.

Currently, the viewDidLoad method does nothing but send its superclass the same viewDidLoad message (you removed the configureView message in Listing 13-7).

  - (void)viewDidLoad
{
    [super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
}

Add the code in bold in Listing 13-12 to viewDidLoad in DetailView Controller.m.

Listing 13-12: Update viewDidLoad

  - (void)viewDidLoad
{
  [super viewDidLoad];

  if (self.popOverButton) {
   if ([[self.splitViewController.viewControllers 
                                          lastObject]
          isKindOfClass:[UINavigationController class]])           {
      [self.navigationItem setLeftBarButtonItem:self.popOverButton  
                                            animated:YES];
    }
    else {
      NSMutableArray *itemsArray = 
                         [self.toolbar.items mutableCopy]; 
      [itemsArray 
               insertObject:self.popOverButton atIndex:0]; 
      [self.toolbar setItems:itemsArray animated:NO]; 
    }
  }
}

When the view is loaded, it’s checked to see whether it contains a popover button. If it does, it simply uses the same logic you see in splitView
Controller: willHideViewController:withBarButtonItem:forPopoverController: (Listing 13-9) using the button that you saved insplitViewController:willHideViewController: withBarButtonItem:forPopoverController: in the popOverButton property.

Finally, you can add what you've done to the TestDriveController. Update the TestDriveController.h interface to make the TestDrive Controller a subclass of the DetailViewController by deleting the commented out bold, underline, and italic code and adding the bolded code in Listing 13-13.

Listing 13-13: Update the TestDriveController.h Interface

  #import <UIKit/UIKit.h>
#import "DetailViewController.h"

//@interface TestDriveController : UIViewController
@interface TestDriveController : DetailViewController 

@end

Adding the Toolbar

Finally, to eliminate the Navigation controller in the Detail view and set it up so that you always see the button to display the Master view in a popover (Road Trip, in this case), you’ll need to add the toolbar to the Test Drive controller in the storyboard and create an outlet to the toolbar. The outlet is needed to be able to add and remove the Road Trip button (and other buttons you’ll add along the way).

Follow these steps to add the toolbar that supports the RoadTrip button (needed by the popover) to the Test Drive controller.

1.     In the Project navigator, select Main_iPad.storyboard, and in the View selector, display the Utility area.

2.     Select the segue from the Master View controller (it is really from the Table View cell, but you can’t see that) to the Test Drive controller in the Canvas to display it in the Attributes inspector.

Optionally, you could select Push Segue from Table View Cell to TestDrive in the Master View Controller – Master Scene in the Document Outline.

image If the Attributes inspector isn't visible, select it in the Inspector selector.

3.     In the Attributes inspector, choose Replace from the Style drop-down menu, choose Detail Split from the Destination drop-down menu, and enter TestDrive in the Identifier field, as I have in Figure 13-11.

You’ll notice that doing so resizes the view so it can display in the Detail view. That means Test Drive will no longer be displayed in the Master view. If you really want Test Drive to still display in the Master view, you can take that on as your personal exercise.

image

Figure 13-11: Update the segue.

4.     In the Document Outline (or in the Canvas — as you can see you can use either), select the SeeTheUSA Image View in Test Drive Controller – TestDrive Scene.

5.     Pin the image view to the edges of the screen with EditorPin using these four subcommands: Leading Space to Superview, Trailing Space to Superview, Top Space to Superview, and Bottom Space to Superview.

6.     Drag in a toolbar from the Library in the Utility area and delete the Item button (it’s included in the toolbar by default), as I have in Figure 13-12.

image In the Library, the toolbar item is way down at the bottom of the gallery. Don’t confuse the toolbar item with a Navigation bar.

You're getting there, but now you’ll need to connect the toolbar to the outlet in the DetailViewController base class.

image

Figure 13-12: The new toolbar.

7.     Select the Test Drive controller in the Document Outline and open the Connections inspector.

image You’d open the Connections inspector as you’d open any inspector: by clicking its icon in the Inspector selector.

8.     In the Connections inspector, drag from the Toolbar outlet to the toolbar on the canvas, as I have in Figure 13-13.

You may remember that you added the Toolbar outlet to the Detail
ViewController base class in Listing 13-4.

Because you deleted the DetailViewController’s detailDescription
Label outlet in Listing 13-4, you’ll have to delete it in the storyboard as well. (If you don’t, you get a runtime error. Just Control-click the Detail View controller entry in the Document Outline to open the Connections window, as I have in Figure 13-14.) You’ll see a yellow warning triangle next to the detailDescriptionLabel line in the Outlets section of the window. Simply delete that outlet by clicking the x in front of the Label – Detail view content goes here line.

Because you’ve added the toolbar, you’ll have to change the math controlling the route your car takes in the TestDriveController.

Update the TestDriveController’s testDrive: method in TestDrive
Controller.m with the code in bold in Listing 13-14.

image

Figure 13-13: Set the toolbar outlet.

image

Figure 13-14: An unused outlet.

Listing 13-14: Take the Toolbar into Account

  - (IBAction)testDrive:(id)sender {
  
  AudioServicesPlaySystemSound(burnRubberSoundID); 
  [self performSelector:@selector(playCarSound)  
                          withObject:self afterDelay:.2]; 
  
  CGPoint center = CGPointMake(car.center.x,
           
self.view.frame.origin.y +
           
car.frame.size.height/2
           
+ self.toolbar.frame.size.height);
  
  void (^animation)() = ^(){
    
    car.center = center;  
  };
  
  void (^completion)(BOOL) = ^(BOOL finished){
    [self rotate];
  };
  
  [UIView animateWithDuration:3 animations:animation 
                                  completion:completion];
}

Adding the button when the view controller is replaced

While all of this gets you close to wrapping up the whole toolbar business, you still need to do one more important thing — and a couple of minor things. The first of these — the important one — I cover in this section. Because you’re replacing the view controller with a new one — albeit one from the same base class — the new view controller has no access to the button it needs to display or the popover it needs to dismiss. Fortunately, the folks at Apple provide a perfect place to do that.

When a segue is triggered — which you do when you tap a Table entry — but before the new controller slides its view into place, the storyboard runtime calls the current view controller’s method so that it can pass data to the view controller that's about to be displayed. That means that you can pass the button information on to the new view controller before it even gets launched. Then, prepareForSegue:sender:, the method you override in Listing 13-15, will both assign the button to be used in viewDidLoad (Listing 13-12) and (if necessary) dismiss the view controller as well.

image prepareForSegue:sender: is a view controller method that notifies a view controller that a segue is about to be performed. segue is the UIStoryboard
Segue object, and it contains information about the view controllers involved in the segue.

Although default implementation of this method does nothing, UIView
Controller subclasses can override it and pass data to the view controller that's about to be displayed. The segue object has properties that point to the source view controller as well as the destination view controller. The segue is the only object that simultaneously knows about both the source and the destination view controllers.

sender is the object that caused the segue, but you won’t need to use it here.

You add the code to the prepareForSegue:sender: method to make the DestinationController the UISplitViewController delegate and assign the popOverButton and masterPopoverController properties.

You’ll also dismiss the popover when it's present so the user doesn’t have to touch in the view to get rid of it.

To do all that, add the code in Listing 13-15 to MasterViewController.m.

Listing 13-15: Overriding prepareForSegue:sender:

  - (void)prepareForSegue:
            (UIStoryboardSegue *)segue sender:(id)sender  
         { 
  if ([[UIDevice currentDevice] userInterfaceIdiom] ==  
                                UIUserInterfaceIdiomPad) 
         {
    DetailViewController *currentDetailViewController;
    if ([[self.splitViewController.viewControllers 
                                          lastObject] 
isKindOfClass:[UINavigationController class]]) 
      currentDetailViewController = 
        (DetailViewController *) ((UINavigationController *)
             [self.splitViewController.viewControllers 
           lastObject]).topViewController;
    else
      currentDetailViewController = 
[self.splitViewController.viewControllers   
           lastObject];
           if(currentDetailViewController.masterPopover 
    Controller 
                                                   != nil) 
              [currentDetailViewController.masterPopover
   Controller 
           dismissPopoverAnimated:YES];
    
    DetailViewController 
           *destinationDetailViewController; 
    
    if ([segue.destinationViewController 
isKindOfClass:[UINavigationController class]])
      destinationDetailViewController = 
        (DetailViewController *)
          ((UINavigationController *)
       segue.destinationViewController).topViewController;  
    else
       destinationDetailViewController = 
           segue.destinationViewController;  
    self.splitViewController.delegate = 
                         destinationDetailViewController;
    destinationDetailViewController.popOverButton = 
             currentDetailViewController.popOverButton;
    destinationDetailViewController.
                                 masterPopoverController 
= 
    currentDetailViewController.masterPopoverController;
  }
}

You start by finding the current Detail View controller because it has the button and popover properties the new view controller needs.

This code appears more complicated than it is. You use the very same logic you used earlier (say, in Listing 13-8) to decide whether you’re faced with a Navigation bar or toolbar. That means I first check to see whether I’m running on an iPad. If I am, I get the current Detail View controller by accessing the list of view controllers in the splitViewController’s viewController array. Fortunately, the UIViewController class has a splitViewController property to make that easy for me.

I then check to see whether a Navigation controller is in the viewControllers array and, if one is there, I get the Navigation controller’s topViewController (the current view controller); if not, I simply use the controller in the array. (You’ll notice a whole lot of casting going on here; I leave it to you to work your way through it.)

  if ([[self.splitViewController.viewControllers 
lastObject]  
            isKindOfClass:[UINavigationController 
class]]) 
  currentDetailViewController =
    (RTDetailViewController *)((UINavigationController *)
            [self.splitViewController.viewControllers      
           lastObject]).topViewController;
    else
      currentDetailViewController = 
[self.splitViewController.viewControllers 
           lastObject];

Next, if I see a popover, I want to dismiss it. I check to see whether a popover controller (that’s why you had to make the masterPopoverController property accessible by moving it into the header file in Listing 13-4) is currently there, and if I see one, I dismiss it.

  if (currentDetailViewController.masterPopoverController 
                                                   != nil) 
      [currentDetailViewController.masterPopoverController  
      dismissPopoverAnimated:YES];

Next, I find the new destination controller (the one being transitioned to) using logic similar to the logic I used to find the current Detail View controller.

  if ([segue.destinationViewController 
isKindOfClass:[UINavigationController class]])
destinationDetailViewController =
  (DetailViewController *)((UINavigationController *)
       segue.destinationViewController).topViewController;  
else
  destinationDetailViewController = 
                         segue.destinationViewController;  

Then I simply set the Split View Controller delegate to the new view controller, so it will get the splitViewController:willHideViewController: withBarButtonItem:forPopoverController: and splitViewController:willShowViewController:invalidatingBarButtonItem: messages.

  self.splitViewController.delegate =    
          destinationDetailViewController;

Finally, I assign the popOverButton and masterPopoverController properties in the new view controller.

  destinationDetailViewController.popOverButton = currentDetailViewController.popOverButton;
destinationDetailViewController.masterPopoverController = 
      currentDetailViewController.masterPopoverController;

Admittedly, this just dismisses the popover and assigns the properties, but doesn’t do anything to display the button. That actually gets done in viewDidLoad — which you added earlier in Listing 13-12.

image You also can specify the size of the popover window by assigning a value to the preferredContentSize property. You should be aware that the actual size may be reduced so that the popover fits on the screen and that the popover does not overlap a keyboard when a keyboard is presented. You can see the code that does that in the MasterViewController’s awakeFromNib method.

  - (void)awakeFromNib
{
  if ([[UIDevice currentDevice] userInterfaceIdiom] == 
                                UIUserInterfaceIdiomPad) 
{
      self.clearsSelectionOnViewWillAppear = NO;
      self.preferredContentSize = 
                                 CGSizeMake(320.0, 600.0);
  }
    [super awakeFromNib];
}

The awakeFromNib message is sent to an object that has been instantiated from the storyboard after all the objects have been loaded and initialized. When the message is sent, all its outlet and action connections have been set.

If you decide to ignore the advice in this section and don’t dismiss the popover controller, taps outside the popover window will cause the popover to be dismissed. You can, however, allow the user to interact with the specified views and not dismiss the popover, using thepassthroughViews property (although you won’t be doing that here). You’d then have to dismiss the popover yourself.

A Few More Tweaks to the MasterViewController

You're almost, but not quite, done. Right now, when you make a selection in the Table view, it stays highlighted. To fix that, you’ll need to implement a Table view method tableView:didSelectRowAtIndexPath:. Add the method in Listing 13-16 to MasterViewController.m to unhighlight a selected cell.

Listing 13-16: Deselect the Cell

  - (void)tableView:(UITableView *)tableView 
       didSelectRowAtIndexPath:(NSIndexPath *)indexPath  
          {
  
  [tableView deselectRowAtIndexPath:indexPath 
           animated:YES];
}

This is a Table View Delegate method that is invoked when the user selects a cell. Normally, you’d launch a view controller here or do something else. (I explain more about this method and other Table View Delegate and Data Source methods in Chapter 20.) But because you’re using a segue, the only thing you need to do is deselect the cell that was tapped, which will remove the highlight.

A partner in crime here is the self.clearsSelectionOnViewWill
Appear = NO; statement you see in the MasterViewController’s awakeFromNib method I mention in the previous section. If that were set to YES, a highlighted selection wouldn't stay highlighted when a view appeared. I have you deselect it here so you begin to get a feel of how things are connected in a Table view — and that I explain in detail in Chapter 20.

And (a Little Extra) One More Thing

Although tapping the Test Drive cell now replaces the Detail View controller with the Test Drive controller, the gesture recognizer still works. The only problem with it is that now you have both a Navigation bar and a toolbar.

To fix that, you’ll need to duplicate the Test Drive controller in the storyboard (select it and then choose Edit⇒Duplicate from the main menu) and give it a different identifier (TestDriveNavigation, for example). Remove the toolbar and change the image view origin and size in the Size inspector back to where it was when you started this chapter (origin of x = 0, y = 0, and height of 1004 — the change in height has to do with the fact that this is now just a view controller without any segues that influence the size). Then change handleSwipeGesture in Listing 13-2 to instantiate the new view controller in MasterViewController.m.

Delete

  UIViewController *viewController = 
   [storyboard instantiateViewControllerWithIdentifier:
          @"TestDrive"];  

and replace it with

  UIViewController *viewController = 
  [storyboard instantiateViewControllerWithIdentifier:
                @"TestDriveNavigation"];

You should also add the following to TestDriveController to keep the popover the right size in Portrait orientation:

  - (void)awakeFromNib
{
  if ([[UIDevice currentDevice] userInterfaceIdiom] == 
                               UIUserInterfaceIdiomPad) {
    self.preferredContentSize = 
                                 CGSizeMake(320.0, 600.0);
  }
  [super awakeFromNib];
}

Well, that concludes your long and winding tour of Split View controllers. Although it may have been a bit arduous, at this point you have a firm understanding of how Split views and popovers work together — one that you can apply to whatever you want to do in your own app.

Before you do that, however, you’ll want to complete the storyboard so you can start adding features and functionality to RoadTrip (besides the Test Drive functionality). You’ll also want to change the Detail view that appears when you launch the RoadTrip app.

Don’t Forget the iPhone

This chapter was primarily about how to handle the Split View controller used in the iPad user interface. The iPhone doesn’t use a Split view, but it does use the TestDriveController. In the last section of this chapter, you added another TestDriveController scene to the iPad storyboard with a TestDriveNaviagtion identifier. This second scene does not need a toolbar, because it is launched with a swipe gesture.

Well, you need that same "toolbarless" scene for using swipe gestures on the iPhone. In fact, the handleSwipeGesture: method shown in Listing 13-2 still works fine on the iPhone, so you don’t have to do any extra work. It’s a bit odd, because it uses part of the iPad storyboard on the iPhone, but it works.