Navigation Controllers II: Split View and the iPad - Learning iOS Development: A Hands-on Guide to the Fundamentals of iOS Programming (2014)

Learning iOS Development: A Hands-on Guide to the Fundamentals of iOS Programming (2014)

Chapter 11. Navigation Controllers II: Split View and the iPad

iPads account for almost half of iOS devices. As of June 2013, there were more than 600 million total iOS devices. Depending on the statistics you look at, iPads account for anywhere from 40% to 60% of those units. All iPhone apps will run on iPad, but they do so in a special mode. For the user, the app screen is centered on the display at actual or double size. Either experience is less than optimal. Creating a universal app, one that uses appropriate screen layouts and graphics on both iPhone and iPad, gives you the best chance of reaching customers using either handheld or tablet devices.

If you have not used an iPad, or even if you have, you might wonder why you need to do something different for a tablet. The basic answer is that the screens are much bigger. They can show more stuff, so they do not have the same restrictions as the smaller phone screens. Currently, the CarValet app is optimized for small spaces; now you need to adapt your user experience for a larger screen.

For navigation-based apps like CarValet, iPad’s UISplitViewController is the controller of choice. It takes full advantage of the iPad screen by maximizing the detail area, while still providing a place for navigation, typically a table view–based hierarchy. Settings, Notes, and Mail are just some of the many apps that use this versatile controller.

After becoming familiar with the basics of the split view controller, you add one to the iPad storyboard. Next, you focus on navigation, starting with the car list. You add the other app sections and some nice animations as the user moves between them. To complete your investigation of navigation, you add the code to work in both landscape and portrait.

With navigation done, you move on to adapting the detail screens for iPad. To do this, you both create new screens and reuse old ones. You learn ways to choose which approach to take. Finally, you hook it all together so the user can do everything in CarValet the same as on iPhone but in a way that suits the iPad experience.

Split View Controller

UISplitViewController is very simple, defining only three public properties and no public methods. Even the delegate protocol has only four methods. The power is in how split view controller manages two other view controllers: one for a master or menu view, and another for the detail.

Figure 11-1 shows these three controllers in the iPad Mail app. The split view controller, outlined in green, is the app window root view. Inside are the master view controller on the left outlined in red and the detail view controller on the right in blue.

Image

Figure 11-1 Split view controller

Figure 11-2 shows the same controller in portrait. On the left side, the master view controller appears to be missing, but it is still there, just not visible. Tapping the upper-left navigation bar button opens the navigation controller popover, as shown in the right-hand image. As long as you support the UISplitViewControllerDelegate protocol and write a little code, most of the work of showing and hiding the master controller is done for you. You can also choose to not show it or to show it all the time.

Image

Figure 11-2 Portrait split view controller with master view hidden and shown

To use a split view controller, you need to do the following:

1. Create master and detail view controllers.

2. Set up master view navigation.

3. Implement the UISplitViewControllerDelegate protocol.

4. Show appropriate content in the detail view, based on the state of the master view.

Creating the master and detail controllers depends on what kind of content is displayed. Most of the time both are UINavigationControllers, enabling flexible navigation as well as adding navigation bars.

Master view navigation is easy to set up using a subclass of table view controller for the root. For navigation, you can use either segues or tableView:didSelectRowAtIndexPath: to push new table views. The navigation controller handles moving back in the menu hierarchy.

Unless you do not need to show the master view in portrait mode, you need to implement the UISplitViewControllerDelegate protocol. Minimally, it needs to allow showing the master view in both orientations. More likely, you will use it to manage the popover, including adding a navigation bar button for showing the popup.

The last step ties everything else together. When a user taps on a leaf node in the hierarchy, you need to update content in the detail controller. When the user taps a car, you show the car, and tapping About shows the About screen. The important thing to understand is that there is no default connection between the master and detail views. You have to do the work required to show appropriate detail, based on whatever is tapped, or change the detail when something else, such as a different car, is selected. And because the detail view is likely a navigation controller, you have to hide the Back button and make sure you are not accidentally pushing or popping too many view controllers.

The next section takes you through adding the split view and creating the master and detail view controllers, as well as some changes required to run the application.

Adding a Split View Controller

CarValet is already configured to be a universal app. At the moment, you cannot run it on an iPad, but as you will see soon, that is only because of some tab bar-specific code. Without that code, you would see a blank screen. This makes sense because the storyboard for iPad, added by Xcode as part of the initial project, is a view controller with an empty view.

You can make any new project universal by using the Device popup in Xcode, as shown in Figure 11-3.

Image

Figure 11-3 Creating a universal project

In this section, you add everything needed to show a split view controller with a basic menu and static detail content.

Although you can start from your own version of the project, it is better to use CH11 CarValet Starter, the project provided with the sample code for this chapter. Unlike previous chapters, it has changes beyond the end of Chapter 10, “Table Views II: Advanced Topics.” All the changes are organizational, using Xcode’s grouping feature to put related files into folders that indicate their purpose. Figure 11-4 shows the updated grouping. If you want to use your own project, the next paragraph and figure summarize the changes.

Image

Figure 11-4 New project organization

The Model folder contains the Core Data model and CDCar files. View contains the custom car table view cell. The Controller folder has the bulk of the files, including related storyboards, protocols, and XIB files. Some projects split this folder further by grouping related controllers into folders. Finally, the Old folder has files that were part of the older projects but are no longer used. In a real development project using source code control (or indeed the project used to create the sample code), these files would be deleted because they are archived. They are left in the samples in case you need to refer to something in those files. The Supporting Files folder is untouched.

To add an iPad group for the classes, follow these steps:

1. Select the CarValet project group folder.

2. Create a new group either by choosing File > New > Group, pressing Cmd-Option-N, or Ctrl-clicking the folder and choosing New Group.

3. Rename the folder iPad and move it below the Controller group.

Adding the Split View Controller

To add the split view controller, you need to first remove any existing controllers and then set up the split view, master, and detail controllers. Follow these steps:

1. Open Main_iPad.storyboard in the standard editor configuration.

2. Select the existing view controller and delete it.

3. Drag the split view controller onto the storyboard. Doing this adds four controllers and three relationships, as shown in the zoomed-out storyboard of Figure 11-5.

Image

Figure 11-5 Split view on the storyboard

The split view controller is added on the left. The top relationship is the master view controller. The current version of Xcode defaults to a navigation controller with a table view as the root. The bottom relationship is the detail view, which defaults to a simple view controller.

4. Replace the existing detail view controller shown on the lower right in Figure 11-5 with a UINavigationController. Removing the existing controller might require clicking outside any controller to deselect everything before selecting the detail controller. When selected, press the Delete key to remove it. Then drag out a UINavigationController from the object library.

5. Drag a connection from the split view controller to the new navigation controller and set it to a detail view controller relationship segue, as shown in Figure 11-6.

Image

Figure 11-6 Detail relationship segue

6. Use the Attributes inspector to set the Size simulated metric of the new navigation controller to Detail. Now the Storyboard editor shows connected controllers in the correct size in portrait or landscape. You can see this by setting the simulated orientation of the split view controller to Landscape. Both the navigation controller and the root view controller change size. Change the orientation back to Inferred.

7. Delete the default table view controller added for the detail navigation controller.

8. Add a UIViewController and then set it as the root view controller for the detail navigation controller by dragging out a relationship segue.

You now have an iPad storyboard with a split view controller. Both the master and detail parts of the split view use navigation controllers, but neither has any content. Select the iPad simulator, make sure you have a breakpoint to catch all exceptions, and run the app. (If you are not sure how to set a breakpoint for all exceptions, see the “Another Useful Breakpoint” section in Chapter 14, “Instruments and Debugging.”)

The CarValet app crashes in application:didFinishLaunchingWithOptions: while trying to set up a tab bar. But the iPad app doesn’t use a tab bar. The solution is to set up the tab bar only for an iPhone. Your first thought might be to check the specific device your app is running on and compare that to known devices to see if it is an iPhone or iPad. That would only work as long as your list of known devices was up to date, something that’s very hard to do.

Instead of checking for the type of device, what you really need to know is whether the interface is a kind of phone or not. iOS provides a utility routine to check the idiom, or kind, of interface. The call is in comment 2 in Listing 11-1. Follow these steps to set up the tab bar only on devices using the phone idiom:

1. Open AppDelegate.h and add the following line of code above the @interface statement:

@class AboutViewController;

2. Define the aboutViewController property by inserting this line of code below the definition of window:

@property (strong, nonatomic) AboutViewController *aboutViewController;

3. Change the top part of application:didFinishLaunchingWithOptions: in AppDelegate.m to the code shown in Listing 11-1. Updated code is shown in bold.

Listing 11-1 Checking for iPad


- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

self.aboutViewController = [[AboutViewController alloc] // 1
initWithNibName:@"AboutViewController"
bundle:[NSBundle mainBundle]];

if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone) { // 2
UITabBarController *tabBarController =
(UITabBarController*)self.window.rootViewController;

UITabBarItem *aboutItem = [[UITabBarItem alloc]
initWithTitle:@"About"
image:[UIImage imageNamed:@"info"]
tag:0];
[self.aboutViewController setTabBarItem:aboutItem];

NSMutableArray *currentItems;
currentItems = [NSMutableArray
arrayWithArray:tabBarController.viewControllers];

[currentItems addObject:self.aboutViewController];
[tabBarController setViewControllers:currentItems animated:NO];
}

UIColor *mocha = [UIColor colorWithRed:128.0/255.0 green:64.0/255.0
blue:0.0 alpha:1.0];
...


Here’s what happens in the numbered lines in Listing 11-1:

1. The about view is used on both iPhone and iPad, so always create it and assign it to a public property.

2. Set up the tab bar only if running on an iPhone (a phone-type device).

Run the app again and everything should work, though there is not much to see. Next, you start adding content by adding the main menu.


Tip: See the Whole Simulator

If you have a large enough screen, showing an iPhone 5 simulator full size works. On a laptop, that is a challenge. For an iPad retina, even a large screen can be too small. The secret is using the Window > Scale menu item to show a scaled-down version of the device. The default choices and their keyboard shortcuts are 100% (Cmd-1), 75% (Cmd-2), or 50% (Cmd-3).


Adding App Section Navigation

The CarValet app has three main sections: Cars, Images, and About. The iPhone uses a tab view for navigation between sections. You could use the same controller on an iPad, but it would not look very good. Tabs are designed for a small screen space. On iPad, each tab would be extremely wide, giving the screen a strange look.

This is one of the reasons the split view controller was created. It allows the same kind of section navigation but fits better in the larger screen. You already have a table view for navigation; the next step is adding the cells for each section.

The top-level menu stays the same, so you don’t need to dynamically generate the cells. Instead, you can use a feature of UITableViewController for creating static cells (as you saw in Chapter 8, “Table Views I: The Basics”). It is a good idea for universal apps to have common elements between the iPhone and iPad versions since a customer could use both. You can use the images from the tab bar buttons for the menu table cells as well as the same section titles. When you are done with these steps, the menu should look as shown in Figure 11-7.

Image

Figure 11-7 The main navigation menu

1. Open the iPad storyboard if it is not already open, and expand the contents of the table view controller in the left-hand list. Select the table view and open the Attributes inspector in the utilities area.

2. Change the content of the table from Dynamic Prototypes to Static Cells, as shown in Figure 11-8. A table view section, containing three cells, is added.

Image

Figure 11-8 Setting a table to Static Cells

3. If there are more or fewer than three cells, select Table View Section in the left-hand list of view controllers and set the Rows to 3.

4. Drag a UIImageView into the first cell. Use constraints to set the width and height to 34 points, and then move the left edge so it is the system distance from the left edge of the cell. Finally, vertically center the image view. Make sure you update the frame.

5. Drag a label into the cell. Set the constraints to vertically centered in the cell, and the system distance from the image view and trailing container edge. Again, make sure to update the frame.

6. Select the image view and label, and Option-drag a copy into the next cell, using the same spacing. Do this once more for the final cell. Update the constraints in the last two cells to be the same as the first one.

7. Set the labels and images as shown in Table 11-1.

Image

Table 11-1 Main Menu Images and Labels

8. The images look distorted because the display mode defaults to Scale To Fill, so for each image view, set the scale mode to Center, as shown in Figure 11-9.

Image

Figure 11-9 Setting the view mode to Center

9. Select the first cell and use the Attributes inspector to set Accessory to Disclosure Indicator.

If you get the yellow warning for constraints or if the image and label disappear for the first cell, update the frames for the views in that cell.

Run the app, and if needed, rotate the device so it is shown in landscape. The three menu items are shown and selectable. Now it is time to add a bit more customization.

Tints, Title, and Generic Detail

The CarValet app still looks fairly generic. To change that, you can customize the navigation bar tint and add a title. Follow these steps:

1. Instead of manually changing the tint for each navigation bar, use the appearance mechanism you used for buttons. Open up AppDelegate.m and add this code to the bottom of application:didFinishLaunchingWithOptions:

UIColor *sky = [UIColor colorWithRed:102.0/255.0 green:204.0/255.0
blue:1.0 alpha:1.0];
[[UINavigationBar appearance] setBarTintColor:sky];

2. Clear the text in the title of the main menu navigation bar. Double-click the existing Root View Controller text and delete it.

3. Set the title of the detail controller bar to CarValet. The double-clicking trick does not work here because there is no text. Instead, select the navigation item from the left-hand list and use the Attributes inspector to set the title.

Now it is time to make the default detail view more interesting. Follow these steps:

1. Import the CH11 Assets Detail Default folder that comes with the code for this chapter into Images.xcassets. The image is a photo of the overflow valet parking tent outside Infinite Loop 1, the worldwide headquarters of Apple. Both normal and retina resolutions are included.

2. Open the iPad storyboard and replace the UIView in the root detail view with a UIImageView.

3. Set the image of the new view to Detail Default and the mode to Aspect Fill. If you do not set the mode, the image appears squished in landscape.

Run the app again. This time you should see the new colors and the nicer looking default detail view. Next, you add the about view and enable the portrait popover menu.

Adding About

Tapping a menu item should cause the CarValet app to behave similarly to tapping a tab bar item. If the content is not showing in detail, tapping should cause it to show. If the content is already showing, then unlike in the tab bar, tapping should do nothing.

Although you have only created three menu items, there are really five menu states, since Cars shows the list of cars. Table 11-2 lists what is in the detail view for each state.

Image

Table 11-2 Detail View Content for App States

The detail view is a navigation controller, so you can push, pop, and even set the array of view controllers. Two of the detail items, About and Images, already exist, so there is no need to re-create them in the iPad storyboard. This means you do not use segues to transition.

Creating MainMenuViewController

Creating a custom class for a table using static cells is much easier than creating one using dynamic cells. The system handles all the methods to determine number of sections, determine number of rows, and create cells. All you need is tableView:didSelectRowAtIndexPath: for acting on user selections. Add the functionality for the main menu by following these steps:

1. With the iPad folder selected, create a subclass of UITableViewController called MainMenuViewController.

2. In the .m file, replace the contents with the following:

#import "MainMenuViewController.h"

#import "AppDelegate.h"

@implementation MainMenuViewController

- (void)viewDidLoad {
[super viewDidLoad];

}

#pragma mark - Table view delegate

- (void)tableView:(UITableView *)tableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath {

}

@end

3. Add the following #defines to MainMenuViewController.h below the #import:

#define kPadMenuCarsItem 0
#define kPadMenuImagesItem 1
#define kPadMenuAboutItem 2

4. In the Layout editor, select the table view controller and use the Identity inspector to set its class to MainMenuViewController.

5. Change tableView:didSelectRowAtIndexPath: to the code in Listing 11-2.

Listing 11-2 Showing the About View


- (void)tableView:(UITableView *)tableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath {

AppDelegate *appDelegate = [UIApplication sharedApplication].delegate;
UIWindow *mainWindow = appDelegate.window;
UISplitViewController *splitViewController = (UISplitViewController*) // 1
mainWindow.rootViewController;
UINavigationController *detailController = (UINavigationController*) // 2
[splitViewController.viewControllers lastObject];

UIViewController *nextController;

switch (indexPath.row) {
case kPadMenuCarsItem:
[detailController popToRootViewControllerAnimated:YES]; // 3
break;

case kPadMenuImagesItem:
[detailController popToRootViewControllerAnimated:YES];
break;

case kPadMenuAboutItem: // 4
nextController = (UIViewController*)appDelegate.aboutViewController;

if (![detailController.topViewController
isMemberOfClass:nextController.class]) { // 5
[detailController pushViewController:nextController // 6
animated:YES];
}

break;
}
}


Here’s what happens in the numbered lines in Listing 11-2:

1. The split view controller is the root view controller of the main app window.

2. A split view controller has two view controllers. The first is the master, and the second, or last, is the detail controller.

3. If the user tapped the Cars or Images menu item, show the default detail image.

4. The user selects the about view, so get a reference from the app delegate.

5. Check if the about view is already showing. You can do that by looking at the class of the controller currently displayed in the detail navigation controller. If the class is the same as the about view class, then it is already being shown.

6. The about view is not shown, so push it into the detail area.

Run the app, make sure the simulator is in landscape, and try tapping the different menu items. Tapping the About menu shows the about view, and tapping on a different item either pops the about view off the stack or leaves the default detail image in place.

There are two issues with the navigation bar for the about view. First, there is no title. Second, the Back button appears. You can fix both these issues in AppDelegate.m application:didFinishLaunchingWithOptions: by adding the following else condition after the check for an iPhone (the new code is in bold):

...
[tabBarController setViewControllers:currentItems animated:NO];
} else {
self.aboutViewController.navigationItem.hidesBackButton = YES;
self.aboutViewController.navigationItem.title = @"About";
}

Run the app again. This time the About item has the correct title and no Back button. Rotate the device to portrait, and the navigation bar still has no button to access the menu; that is the next thing you add.

Polishing Menu Images

In previous chapters, you used images for selections and there was not that much difference in the images. Whether it is a tab bar or the menu for the iPad, using a different image to indicate a selected state helps make things obvious. It is also good to show those using the right tint from the app color scheme.

There are two main parts to making selected images that use a custom tint work:

Image Set the highlighted image.

Image Update both images to use the template rendering mode.

The first part is adding images for the selected state:

1. Open Images.xcassets and import CH11 Assets Selected Icons.

2. In the iPad storyboard, select the car image from the menu table view. Use the Attributes inspector to set the highlighted image to car-selected.

3. Set the tint for the image to mocha (or whichever color you have chosen).

4. Set the highlighted image and tint for the two remaining menu cells. The highlighted image name is always the name of the regular image appended with “-selected.”

Run the app in landscape and tap on each cell. You should see the image change, though it is still the same color as the source artwork. The next step is to change the tint.

Changing the Image Tint

The graphical data in an image can be used in two basic ways. The most typical way is to specify how to draw the image: the size, colors, opacity, and more. The other is as a template for showing something else. You have probably seen things like this as an image appears to change colors, or pulse, or other tricks. It is the same image and the same image data. It is just used in a different way.

In iOS, the renderingMode property of a UIImage tells the system how to use the image data. UIImageRenderingModeAlwaysOriginal says to show exactly what the image specifies. If it is a light blue stylized car, show a light blue stylized car.

UIImageRenderingModeAlwaysTemplate says to use the graphical data as a template, effectively allowing you to use the same light blue stylized car in whatever color, or tint in iOS, you want. The intensity and transparency of the color at any point on the image is the same, but it is displayed using the tint color.

The default, UIImageRenderingModeAutomatic, lets the system decide how to use the image. The idea is that you can use the same image for buttons and for other purposes. Whenever you set the image in IB, it is created with the default-rendering mode. Much of the time this does what you want, but not always.

The image views in menu table view cells are a good example. What you want is to set the rendering mode of the images. However, at this time there is no way to do that in IB, and the property itself is read-only.

With a small amount of code, you can replace the images with copies that are set as templates:

1. Open the Storyboard and an Assistant editor with MainMenuViewController.h.

2. Select the top car image and drag to the .h file as if you were going to create a property. When the popup comes up, set the type to an Outlet Collection, as seen in Figure 11-10.

Name the property menuImages and make sure it is of type UIImageView.

Image

Figure 11-10 Setting an outlet collection

3. Now Ctrl-drag from the other two image views to the new outlet collection.

You can confirm all three images are in the collection by Ctrl-clicking on the controller icon in the bar below the layout window in IB. Look for an item called Outlet Collections and the property should be there and include all the image views.

4. Open MainMenuViewController.m and add the following code to the end of viewDidLoad:

for (UIImageView *atView in self.menuImages) {
atView.image = [atView.image imageWithRenderingMode:
UIImageRenderingModeAlwaysTemplate];
atView.highlightedImage = [atView.highlightedImage
imageWithRenderingMode:
UIImageRenderingModeAlwaysTemplate];
}

The code iterates through each image view in the outlet collection. For each of those views, it replaces both the image and the highlightedImage properties with a copy. The copy is made with imageWithRenderingMode:, a UIImage method that sets the rendering mode of the copied image.

Run the code, and you can see tinted images that highlight when selected. This technique can be used on more than just image views in cells. It can also be used in tab bars, buttons, and bar buttons—anywhere you can specify an image and a highlighted image.

Accessing the Menu in Portrait

You can access the main/menu controller of a split view in portrait in one of two ways: always show the controller, as the Settings app does, or show a menu popover button for opening the menu, as in Mail and Notes. Either way requires implementing one or more methods of theUISplitViewControllerDelegate protocol.

All the methods support some stage of showing or hiding a menu popover. Always showing the menu is the simplest: You just return NO from splitViewController:shouldHideViewController:inOrientation:.

CarValet hides the menu in portrait, so you need a few more methods. What class do you use for the delegate? The Apple Xcode samples tend to use the detail view controller as the delegate. Other possibilities are the master view controller, the app delegate, or a custom class.

Using the detail controller makes sense when there is only one controller switching between different views. This app has four detail view controllers, not one: no menu item chosen, about, car images, and car detail.

MainMenuViewController is not a good choice because it shows a submenu based on the iPhone storyboard controller (car images). Using the app delegate adds code to an already code-heavy class, and it means you lose the opportunity to create a reusable class. A better choice is to use a custom object. One way to do this is to create an instance of the custom class and associate it with the app delegate. Then any object can access the delegate by using an app delegate property. But there is another way to implement coordination classes that need only one instance: Use a singleton.

A singleton is a special type of class that prevents the creation of more than one instance object. Instead of callers allocating and/or initializing an instance, they call a special class method that returns the single shared object. The same method usually creates the instance if it does not already exist. Typically, it looks as follows:

MySingletonClass *mySingleton = [MySingletonClass sharedMySingleton];

Implementing the DetailController Singleton

In addition to implementing the split view controller delegate protocol, the new class eventually manages the detail content. And even if it did not, most of the changes made by a split view delegate are to the detail view.

First, you implement the singleton functionality:

1. Add a new class called DetailController, based on NSObject, to the bottom of the iPad group.

2. Open DetailController.h and add the following line just above the @end statement. The + sign indicates a class method:

+ (DetailController*)sharedDetailController;

3. Set the contents of DetailController.m to the code shown in Listing 11-3.

Listing 11-3 Detail Controller Implementation


@implementation DetailController

#pragma mark - Singleton

+ (DetailController*)sharedDetailController // 1
{
static DetailController *sharedDetailController = nil; // 2
static dispatch_once_t pred;

dispatch_once(&pred, ^{ // 3
sharedDetailController = [super new];
});

return sharedDetailController; // 4
}

@end


Here’s what happens in the numbered lines in Listing 11-3:

1. The declaration of the class method for returning the shared instance.

2. Create a static variable for holding the single instance after it is allocated.

3. Create the shared instance only once.

4. Return the shared instance.


Note: What Is dispatch_once?

dispatch_once is a handy routine which guarantees that the included code is executed only once. The pred argument is part of the mechanism which ensures that the code is executed once. The routine is part of something called Grand Central Dispatch (GCD), an advanced technology in both iOS and Mac OS.

A discussion of GCD is beyond the scope of this book. For now, you can adapt this code wherever you need a singleton, replacing sharedDetailController with your own appropriate name.

For more information, see the Apple documentation or iOS Components and Frameworks: Understanding the Advanced Features of the iOS SDK by Kyle Richter and Joe Keeley.


Adding UISplitViewControllerDelegate

Adding support for the split view controller delegate protocol takes three steps: Set up the .h file, add the required protocol methods, and set the instance as the split view delegate.

The .h file needs only two additions. The first declares that the class supports the protocol. The second declares a property for the split view controller managed by the delegate. Add these lines below the @interface declaration in DetailController.h:

<UISplitViewControllerDelegate>

@property (weak, nonatomic) UISplitViewController *splitViewController;

For now, you need to implement three of the four delegate methods. splitViewController:shouldHideViewController:inOrientation: determines whether the app should hide the menu. CarValet hides the menu in portrait itself. You add the navigation bar button to the active detail view controller.

splitViewController:willShowViewController:invalidatingBarButtonItem: is called when the split view controller rotates to an orientation that shows the menu. Use it to remove the navigation bar button for showing the popover menu.

You add support for the split view controller protocol methods by following these steps:

1. Open DetailController.m and add local instance variables for the navigation bar menu button, popover, and detail controller by inserting this code just below the @implementation statement:

{
UIBarButtonItem *menuPopoverButtonItem;
UIPopoverController *menuPopoverController;
UINavigationController *detailNavController;
}

menuPopoverButtonItem is the current menu bar button item, used for setting the left navigation bar button when the current detail controller changes. Tapping the button opens the menuPopoverController. detailNavController is the detailUINavigationController.

2. Add the following #pragma below the implementation statement to make it easy to see groupings of functionality in the Function dropdown:

#pragma mark – UISplitViewControllerDelegate

3. Add the following code below the #pragma statement:

- (BOOL)splitViewController:(UISplitViewController *)svc
shouldHideViewController:(UIViewController *)vc
inOrientation:(UIInterfaceOrientation)orientation {
return UIInterfaceOrientationIsPortrait(orientation);
}

4. Continue by adding the code in Listing 11-4 to add the last two protocol methods.

Listing 11-4 Delegate Methods for Hiding and Showing the Menu


- (void)splitViewController:(UISplitViewController *)svc // 1
willHideViewController:(UIViewController *)aViewController
withBarButtonItem:(UIBarButtonItem *)barButtonItem
forPopoverController:(UIPopoverController *)pc {
barButtonItem.title = @"Menu"; // 2

menuPopoverButtonItem = barButtonItem; // 3
menuPopoverController = pc;

UINavigationItem *detailNavItem = // 4
detailNavController.topViewController.navigationItem;
detailNavItem.leftBarButtonItem = barButtonItem;
}

- (void)splitViewController:(UISplitViewController *)svc // 5
willShowViewController:(UIViewController *)aViewController
invalidatingBarButtonItem:(UIBarButtonItem *)barButtonItem {
menuPopoverButtonItem = nil; // 6
menuPopoverController = nil;

UINavigationItem *detailNavItem = // 7
detailNavController.topViewController.navigationItem;
detailNavItem.leftBarButtonItem = nil;
}


Here’s what happens in the numbered lines in Listing 11-4:

1. Called when a rotation hides the main menu.

2. Set the title of the popover navigation bar button to Menu.

3. Set the local instance variables to the menu navigation bar button and popover controllers.

4. Find the navigation bar of the front-most detail controller and set the left button to the menu button.

5. Called when a rotation shows the main menu.

6. Clear the local menu navigation bar button and popover controller.

7. Remove the left navigation bar button item for the current detail controller.

Creating and Setting Up the Singleton

You need to set up the singleton before you can use it. The best place to do this is at application launch since all the view controllers already exist. Follow these steps:

1. Import DetailController.h into AppDelegate.h.

2. Change the else case in application:didFinishLaunchingWithOptions: to set up the detail controller and set the split view controller. Add the following code shown in bold:

} else {
self.aboutViewController.navigationItem.hidesBackButton = YES;
self.aboutViewController.navigationItem.title = @"About";

UISplitViewController *splitViewController =
(UISplitViewController *)self.window.rootViewController;

DetailController *detailController = [DetailController
sharedDetailController];
detailController.splitViewController = splitViewController;
}


Note: Setting Up and Accessing Singletons

In reality, you do not need to set up a singleton by assigning it to an instance variable. A singleton is set up the very first time it is accessed using the shared class method. An instance variable provides a shorthand way to reference the singleton. You could just call the sharedclass method every time, including the first one.


Setting the splitViewController property solves only part of the problem, as DetailController needs a reference to the detail UINavigationController. detailNavController is not a public variable, so it can be set only from inside the instance or by creating a public method.

It would be nice to have the singleton set the variable whenever splitViewController is changed. That would cut down on excess calls by users of the singleton and make the code more reusable. Accessor methods are designed for this purpose. They are called whenever a property is accessed or set using either dot notation or a method call.

Table 11-3 shows the two types of accessors, getters and setters. Each does what the name implies: Getters get the value of a property; setters set the value. When you use @property, the getters and setters are created for you.

Image

Table 11-3 Method Call and Dot Notation Form for Getters and Setters

You can override the defaults by creating your own getter and/or setter. Getter method declarations are always of the form:

- (void) <propertyName>

Setters look like this:

- (void) set<propertyName>:(<ObjectType>)<propertyName>

Add the #pragma and getter method in Listing 11-5 below the implementation and local instance variable declarations in DetailController.m.

Listing 11-5 DetailController Setter for splitViewController


#pragma mark - Setter

- (void)setSplitViewController:(UISplitViewController *)splitViewController {
if (splitViewController != _splitViewController) { // 1
_splitViewController = splitViewController; // 2
detailNavController = // 3
[splitViewController.viewControllers lastObject];
_splitViewController.delegate = self; // 4
}
}


Here’s what happens in the numbered lines in Listing 11-5:

1. Change things only if the new split view controller is different from the current one.

2. Set the current split view controller to the new one. Note the use of the underscore form for the property. Using the dot or set form inside a setter is dangerous. (See the “Warning: Setters and Dot/set == Death.”)

3. Set the private instance variable for the detail navigation controller.

4. Become the split view controller delegate.


Warning: Setters and Dot/set == Death

Setting the value of a property using dot notation or the set form of an accessor is normally safe. There are two exceptions. The first is in object initialization methods. The other case is when you use a setter. First, you need to understand that dot notation is just a shorthand way of specifying the set-based method call.

If you access a property inside the setter using the dot/set form, you are really just calling the setter. In the best case, the system detects the problem and throws an exception. More likely, you enter a recursive loop, calling the setter over and over and over until the system runs out of memory, shuts down your app, or your app crashes.

Whenever you declare an instance variable using @property, in addition to creating setters and getters, Xcode prepends an underscore (_) to the property name to create the real instance variable name. You can safely use the underscore version of the property name inside setters as it is the real memory pointer.


Run the app and rotate the simulator to portrait. The menu view controller disappears, and a menu button appears in the navigation bar of the current detail view. Tapping the menu button shows the menu controller, and tapping outside the menu controller dismisses it. You can also show and hide the menu controller by swiping right.

Use the menu to show different content in the detail area. There are two issues. First, the menu does not automatically dismiss when you select the item. Second, when you dismiss the popover, there is no button to open it again.

Consolidating the Code to Switch Detail Content

You might think you should add more code to MainMenuViewController. Although doing so works, as you add more view controllers, you get lots of repetitive code. Instead, you can consolidate all the work in DetailController.

Changing the current contents of the detail area requires running some code. You could do that by defining a special method in DetailController, or you could define a property and a setter. Follow these steps to use a setter for changing the current detail view controller:

1. Create a strong property called a currDetailController of type UIViewController in DetailController.h.

2. Add the code from Listing 11-6 after setSplitViewController: in DetailController.m.

3. Import DetailController.h into MainMenuViewController.m.

4. Replace tableView:didSelectRowAtIndexPath: with the code shown in Listing 11-7. As well as being cleaner and easier to read, the new method is only two-thirds the size of the original.

Listing 11-6 DetailController Setter for currDetailController


- (void)setCurrDetailController:(UIViewController*)currDetailController {
NSArray *newStack = nil; // 1

if (currDetailController == nil) { // 2
UINavigationController *rootController = // 3
detailNavController.viewControllers[0];

if (detailNavController.topViewController != rootController) { // 4

newStack = @[detailNavController.viewControllers[0]]; // 5

}
} else if (![currDetailController isMemberOfClass: // 6
[detailNavController.topViewController class]]) {

newStack = @[detailNavController.viewControllers[0], // 7
currDetailController];
}

[menuPopoverController dismissPopoverAnimated:YES]; // 8

if (newStack != nil) {
[detailNavController setViewControllers:newStack animated:YES]; // 9

_currDetailController = detailNavController.topViewController; // 10
_currDetailController.navigationItem.leftBarButtonItem = // 11
menuPopoverButtonItem;
}
}


Here’s what happens in the numbered lines in Listing 11-6:

1. If the current detail changes, newStack is set to a replacement stack of view controllers.

2. Setting the new detail content controller to nil shows the default CarValet image.

3. The valet tent image is always the first, or root, controller.

4. Check whether the tent is already showing.

5. The tent is not showing, so set the new stack to just the detail controller that shows the tent image.

6. The new controller is not nil, so check whether it is already showing. Do this by checking whether the current and new controllers are the same class.

7. It is a new detail view controller, so change the navigation controller’s stack. Put the tent view controller on the bottom and the new controller on top.

8. Dismiss the menu popover if it is showing. If it is not showing, this call sends a message to nil, so nothing happens.

9. Show the new detail content.

10. Set the current detail controller to the new one. Again, this is a setter, so use the underscore version.

11. Set the left button of the navigation bar to the menu popover button. If there is no popover button, leftBarButton is set to nil—that is, to no button.

Listing 11-7 Updated tableView:didSelectRowAtIndexPath:


- (void)tableView:(UITableView *)tableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath {

AppDelegate *appDelegate = [UIApplication sharedApplication].delegate;
DetailController *detailController = [DetailController
sharedDetailController];

UIViewController *nextController;

switch (indexPath.row) { // 1
case kPadMenuCarsItem:
nextController = nil;
break;

case kPadMenuImagesItem:
nextController = nil;
break;

case kPadMenuAboutItem:
nextController = (UIViewController*)appDelegate.aboutViewController;
break;
}

detailController.currDetailController = nextController; // 2
}


Here’s what happens in the numbered lines in Listing 11-7:

1. Set the next controller to the correct value or nil for the default tent picture.

2. Let DetailController handle changing or not changing the detail view, as well as dismissing the popover if one is shown.

After the changes, MainMenuViewController is handling behaviors associated with the table. DetailController implements behaviors specific to the split view, including showing and hiding the menu popover. Even when the menu view is shown in the popover, the menu controller itself should not care about where it is presented.

Run the app again, trying it in both landscape and portrait modes. Make sure everything behaves correctly, including switching between view controllers in portrait and landscape, showing and hiding the menu when rotating, and showing and hiding the menu button for the about and tent view controllers, depending on orientation.

The only thing that looks strange is the animation when changing detail content. This is because you are using the UINavigationController default animation. As an alternative, you can use core animation. Explaining the code is beyond the scope of this book. See “Recipe: Using Core Animation Transitions” in Core iOS 6 Developer Cookbook by Erica Sadun for more information.

Add the animation using these steps:

1. Import <QuartzCore/QuartzCore.h> into DetailController.m.

2. Add the QuartzCore framework to your project from Project Settings, just as you added CoreData in Chapter 9, “Introducing Core Data.”

3. Update displaying the new navigation controller in setCurrDetailController: with the new code shown here in bold:

if (newStack != nil) {
CATransition *transition = [CATransition animation];
transition.duration = 0.3f;
transition.timingFunction = [CAMediaTimingFunction
functionWithName:
kCAMediaTimingFunctionEaseInEaseOut];
transition.type = kCATransitionFade;
[detailNavController.view.layer addAnimation:transition forKey:nil];

[detailNavController setViewControllers:newStack animated:NO];

The transition duration matches the iOS animation time for closing the popover. Make sure the navigation controller does not try to animate the change by setting animated: to NO in the call to setViewControllers:animated:.

This time when you run the app, the new detail content fades in. The animation is specified by the transition type property, as shown in Table 11-4. You can try experimenting with other transition types. Check the documentation for different subtypes by searching for CATransition.

Image

Table 11-4 Transition Constants

Adding Car Images

The car images view controller already exists on the iPhone storyboard, so you can load and show it. Doing this takes two steps: adding a storyboard identifier, and loading and preparing the controller, as described here:

1. Open Main_iPhone.storyboard in the Layout editor and select the car image view controller.

2. Set the Storyboard ID to CarImagesViewController in the Identity inspector, as shown in Figure 11-11.

Image

Figure 11-11 Setting the storyboard identifier

When a view controller has a storyboard ID, you can use the UIStoryboard method instantiateViewControllerWithIdentifier: to load an instance.

3. Open MainMenuViewController.m and update tableView:didSelectRowAtIndexPath: with the bold lines shown in Listing 11-8.

Listing 11-8 More Updates to tableView:didSelectRowAtIndexPath:


...
DetailController *detailController = [DetailController
sharedDetailController];
UIStoryboard *iPhoneStory = [UIStoryboard // 1
storyboardWithName:@"Main_iPhone"
bundle:nil];

UIViewController *nextController;

switch (indexPath.row) {
case kPadMenuCarsItem:
nextController = nil;
break;

case kPadMenuImagesItem:
nextController = [iPhoneStory // 2
instantiateViewControllerWithIdentifier:
@"CarImagesViewController"];

nextController.navigationItem.hidesBackButton = YES; // 3
nextController.navigationItem.rightBarButtonItem = nil; // 4
break;
}


Here’s what happens in the numbered lines in Listing 11-8:

1. Load the iPhone storyboard.

2. Set the next controller to a car image controller loaded from the iPhone storyboard.

3. Hide the Back button.

4. Remove the existing Reset Zoom right navigation bar button.

MainMenuViewController does not need to worry if the car image detail view is showing or not. All it does is tell DetailController to set the detail area to the car image view. By using the currDetailController setter, DetailController takes care of checking the current detail view and loading the car image view if it is not already showing. This is a good example of using partitioning of responsibility to minimize the amount of code for adding a new behavior.

When you run the app, the car images controller shows correctly. But as usual, there appear to be a couple issues with adapting the controller to iPad. First, the car number is incorrect. Also, as shown in Figure 11-12, when you swipe through the cars, the car images become increasingly offset.

Image

Figure 11-12 Offset car images

You might be tempted to look at what is happening and possible fixes, but in reality, there is only one issue. If you have access to an iPad, run the app on it. The car images still become increasingly offset; however, the initial car number is correct. There are differences between the simulator and devices, so it is a good idea to try code on a real iPad, especially when you encounter bugs that seem strange. See the “Tip: Why Did This Bug You?” to learn why the car number problem appeared to be simulator-specific.


Tip: Why Did This Bug You?

The first step in solving the car number problem is finding the source. The issue occurs only when the label is first shown, and that happens in viewDidAppear:. Use a breakpoint at the call to updateCarNumberLabel so you can step into that method and look at the state. The presenting issue is that carIndex has the wrong value, implicating carIndexForPoint:.

Instead of rerunning and breaking there, take a look at the relevant properties, and you see that there is an issue with scrollView. In the Debug Console, po self.scrollView quickly shows the issue: The frame is all 0s. The scroll view is not being laid out.

Put a breakpoint in updateCarNumberLabel and let the app continue. Scroll to the next car, and you see that the frame is now initialized.

Now you can test your theory. You might notice that moving the car initializes the frame, so perhaps other movement would as well. Let the navigation controller animate, adding detail content in DetailController.m, by changing NO to YES in one line of thecurrentDetailController setter:

[detailNavController setViewControllers:newStack animated:YES];

This works. The car number shows up correctly, but so does an extra animation. This is the first clue the issue could be simulator-related. Confirm this by adding the following:

[self.view layoutIfNeeded];

before the call to updateCarNumberLabel in viewDidAppear:. Again, the car number label is correct. You can now try out the code on a real iPad, and when you do, you see that it works.


The source of the problem is mentioned in the “Tip: Why Did This Bug You?” Specifically, the scroll view frame is not initialized. Looking through the code, you see that setupScrollContent also relies on an initialized frame to set up the content correctly. And that is not the case during viewDidLoad.

The simple fix is to move both setting the scroll content and showing the initial label to the same view-based method. In this case, you change everything to use viewDidAppear:. In CarImagesViewController.m, cut [self setupScrollContent]; from viewDidLoadand paste it above the call to updateCarLabel in viewDidAppear:. Run the app in the iPhone simulator to make sure the images view still works. Then run it in the iPad simulator. The car images scroll correctly. In both cases, the initial car number is incorrect, but running the app on a device quickly shows you that everything works properly.

Now that Images and About are done, it is time to add the last menu item.

Adding Cars

Unlike About and Images, the first part of adding Cars updates the menu controller. On the iPhone app, the Cars tab has four functions: add cars, remove cars, view cars, and select a car to view detail. Only the last function, viewing a car, changes the detail content.

The first step is showing a car table when the Cars menu item is tapped. There is a car table on the iPhone storyboard, and you already know how to load a controller from there:

1. Add an identifier to the CarTableViewController on the iPhone storyboard.

2. Show the new controller in the menu view. Do this by changing the kPadMenuCarsItem switch case to the code shown in Listing 11-9.

Listing 11-9 Showing a Car Table


...
case kPadMenuCarsItem:
nextController = [iPhoneStory // 1
instantiateViewControllerWithIdentifier:
@"CarTableViewController"];

[self.navigationController pushViewController:nextController // 2
animated:YES];

nextController = nil; // 3
break;
...


Here’s what happens in the numbered lines in Listing 11-9:

1. Load the car table controller.

2. Make the car table the current menu controller.

3. Set the next controller back to nil. This is very important; if you did not do this, the last line of the method would load the car table into the detail area.

When you run the app in portrait, it does not quite do what you want. Open the menu popover, tap Cars, and the car table loads. However, the popup is dismissed. This makes sense because the current detail controller is always set, even if the next controller is nil. And every time the setter is called, the popover is dismissed. This behavior still makes sense; the problem is showing that the car table submenu should have no effect on the detail area.

The simplest solution is to make the call setting currDetailController conditional. A simple BOOL in tableView:didSelectRowAtIndexPath: is all you need:

1. Add the following line above the switch statement:

BOOL newDetail = YES;

2. Change the last line in the kPadMenuImagesItem case to this:

newDetail = NO;

3. Update the last line of the method, based on the bold code:

if (newDetail) {
detailController.currDetailController = nextController;
}

Run the app in portrait and open the menu popover. Choose About, and it behaves correctly, with the popover closing and the detail area updating. Choose Cars; it works as expected, with the popover staying open and the car table showing in the menu area. There is just one problem: There is no way to get back to the main menu. The Edit button appears where the Back button should be.

Adapting the Car Table to iPad

There is no easy way to fix the issue of the Edit button appearing where the Back button should be. Although it is easy to show both the Back and Edit buttons by using leftItemsSupplementBackButton, this does not fix the deeper problem. Tapping a car cell uses a segue to open the detail in the same menu navigation controller.

There are three basic approaches to solving the issue. First, you could make an iPad-specific copy of the view controller class files and make the required changes. As part of this approach, you would also copy the iPhone view controller to the iPad storyboard and make appropriate changes. The second approach is a variant. You still copy the storyboard view controller, but you use subclassing to create an iPad view controller class with only the methods you need.

Because the iPad-specific visual changes are small, a better solution is to modify CarTableViewController to work with both iPhone and iPad. Doing this only requires three basic changes—adding a small protocol for finding the right car to show, adding a delegate to the main menu view controller, and adding or modifying three car table methods—as shown here:

1. Create a new Objective-C protocol file called CarTableViewProtocol.h below CarTableViewController.m.

2. Add an @class declaration for CDCar and one declaration to the new protocol:

- (void) selectCar:(CDCar*)selectedCar;

3. Open CarTableViewController.h and import CarTableViewProtocol.h. Then declare a delegate property in the interface for the class:

@property (weak, nonatomic) id <CarTableViewProtocol> delegate;

4. Only set the title in the navbar if there is no delegate. Add the following if condition around the call to set the title in viewDidLoad (the new code is in bold):

if (!self.delegate) {
self.title = NSLocalizedStringWithDefaultValue(
@"AddViewScreenTitle",
nil,
[NSBundle mainBundle],
@"CarValet",
@"Title for the main app screen");
}

5. Open CarTableViewController.m and remove the viewDidLoad line that sets the left bar button:

self.navigationItem.leftBarButtonItem = self.editButton;

6. Add tableView:didSelectRowAtIndexPath: to send selectCar: to the delegate if one exists. Eventually, this causes the delegate to update the detail content:

- (void)tableView:(UITableView *)tableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if (self.delegate != nil) {
[self.delegate selectCar:[self carToView]];
}
}

7. Modify editTableView: to update buttons on the right side of the navigation bar if there is a delegate by replacing the call to setLeftBarButtonItem:animated: with the code in bold:

UIBarButtonItem *nextButton = (startEdit) ?
self.doneButton : self.editButton;

if (self.delegate == nil) {
[self.navigationItem setLeftBarButtonItem:nextButton animated:YES];
} else {
UIBarButtonItem *addButton = self.navigationItem.rightBarButtonItems[0];

[self.navigationItem setRightBarButtonItems:@[addButton, nextButton]
animated:YES];
}

[self.tableView setEditing:startEdit animated:YES];

8. Create a viewWillAppear: method after viewDidLoad, using Listing 11-10. This method is called after the view is laid out but before it is shown, providing a chance to make final changes.

9. Add the code in Listing 11-11 above prepareForSegue:sender:. If there is a delegate, the method prevents the segue attached to car view cells from firing, resulting in the default table behavior that calls tableView:didSelectRowAtIndexPath:.

10. Add support for the new protocol in MainMenuViewController.h by #importing the file and adding the protocol name in angle brackets after the @interface statement.

11. Open MainMenuViewController.m, import CarTableViewController.h and CDCar.h, and add the following #pragma and method at the end of the implementation:

#pragma mark - CarTableViewProtocol

- (void)selectCar:(CDCar *)selectedCar {
NSLog(@"\nSELECT a car: %@ %@ %@\n\n",
selectedCar.make, selectedCar.model, selectedCar.year);
}

12. Add this code to tableView:didSelectRowAtIndexPath: in the kPadMenuCarsItem case, below where you instantiate nextController from the iPhone storyboard. In addition to setting the title, the code also sets the delegate:

nextController.navigationItem.title = @"Cars";
((CarTableViewController*)nextController).delegate = self;

Listing 11-10 Car Table viewWillAppear:


- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];

if (self.delegate == nil) { // 1
self.navigationItem.leftBarButtonItem = self.editButton; // 2
} else { // 3
UIBarButtonItem *addButton = self.navigationItem.rightBarButtonItem;
self.navigationItem.rightBarButtonItems = @[addButton, self.editButton];
}
}


Here’s what happens in the numbered lines in Listing 11-10:

1. Check whether there is a delegate.

2. If there is no delegate, put the Edit button on the left.

3. There is a delegate, so put both the Edit and Add buttons on the right. Note that the order of buttons in the array is from right to left. The Add button appears last, on the right of the navigation bar.

Listing 11-11 Car Cell Conditional Segue


- (BOOL)shouldPerformSegueWithIdentifier:(NSString*)identifier
sender:(id)sender {
if ([identifier isEqualToString:@"ViewSegue"]) { // 1
if (self.delegate != nil) { // 2
return NO; // 3
}
}

return YES; // 4
}


Here’s what happens in the numbered lines in Listing 11-11:

1. You only need to check if this is a car table cell tap segue indicated by the string @"ViewSegue".

2. Check whether there is a delegate.

3. There is a delegate, so do not fire the segue. When you stop the segue, the default table behavior takes over, calling tableView:didSelectRowAtIndexPath:.

4. Fire the segue because there is no delegate or it is a different segue.

Run the app on the iPhone simulator to confirm that the car table works correctly. When you make changes to a view or view controller that is used in multiple form factors, it is a good idea to test it on each form factor.

Now run the app on iPad. Everything works as expected: The Back button appears and goes to the main menu, Edit and Add work, and tapping a car prints something like the text below in the debugger:

2013-02-12 20:23:52.717 CarValet[1128:c07]
SELECT a car: Honda Accord 2012

Car Detail Controller

You already have a controller to show car detail on the iPhone storyboard. Better still, it uses a protocol to find what car to show and let a delegate know if there are changes. Follow these steps to show car detail:

1. Open the iPhone storyboard and set the ViewCarTableViewController storyboard ID so it’s the same as the class name.

2. Open MainMenuViewController.m and import ViewCarTableViewController.h. Now add instance variables for a current car and current controller below @implementation:

{
CDCar *currentCar;
ViewCarTableViewController *currentViewCarController;
}

3. Add support for ViewCarProtocol to MainMenuViewController and copy the protocol methods to the end of the implementation:

#pragma mark - ViewCarProtocol

-(CDCar*)carToView {
return currentCar;
}

-(void)carViewDone:(BOOL)dataChanged {
NSLog(@"\ncarViewDone\n\n");
}

You have implemented these protocol methods once before for CarTableViewController.

4. Update the selectCar: method using Listing 11-12.

Listing 11-12 Car Cell Conditional Segue


- (void)selectCar:(CDCar *)selectedCar {
NSLog(@"\nSELECT a car: %@ %@ %@\n\n",
selectedCar.make, selectedCar.model, selectedCar.year);

currentCar = selectedCar; // 1

if (currentViewCarController == nil) { // 2
UIStoryboard *iPhoneStory = [UIStoryboard
storyboardWithName:@"Main_iPhone"
bundle:nil];
currentViewCarController = [iPhoneStory
instantiateViewControllerWithIdentifier:
@"ViewCarTableViewController"];

currentViewCarController.navigationItem.title = @"Cars"; // 3
currentViewCarController.navigationItem.hidesBackButton = YES;
currentViewCarController.delegate = self;
}

[DetailController sharedDetailController].currDetailController = // 4
currentViewCarController;
}


Here’s what happens in the numbered lines in Listing 11-12:

1. Set the currentCar local variable to the selected car for use in ViewCarProtocol.

2. If there is no view car controller, load one from the iPhone storyboard.

3. Set up the new controller by giving it a title, hiding any Back button, and setting the MainMenuViewController as the delegate.

4. Tell the detail controller to show the detail car view.

Run the app on the iPad. It almost works. When the detail area doesn’t contain a car detail controller, tapping a car in the table shows car detail. However, tapping a second car doesn’t replace the information. This is because ViewCarTableViewController was designed for a single use. All the fields are set up in viewDidLoad instead of a different method. On the iPhone, changing a car happens when the car detail view controller is gone, so there is no notion of an information update.

No matter what the solution, you need to know when to tell the car detail to update. You do not want to update the first time the controller is shown. You therefore need to detect whether the current detail controller is car detail. If it is, you need to update the content. If it is not, you need to tellDetailController to load a new detail content controller. In MainMenuViewController.m, replace the last line of selectCar: with the following:

DetailController *detail = [DetailController sharedDetailController];

if ([detail.currDetailController
isMemberOfClass:[ViewCarTableViewController class]]) {
[currentViewCarController viewDidLoad];
} else {
detail.currDetailController = currentViewCarController;
}

The if condition checks whether the current detail controller is a view car controller. If it is, viewDidLoad is sent. If it is not, a new view controller is loaded.

Now you can run the app on iPad and watch cars change in the detail view. Editing works, though the main menu view controller is not sent carViewDone: when cars change. It is possible for the main menu controller to detect when cars change, but this defeats the purpose of using a protocol.

You now have two basic choices. The first is to update ViewCarTableViewController, ViewCarProtocol, and MainMenuViewController to be more general. That would include changes to any other users of the protocol. But even if you do that, the presentation does not look good on the larger iPad screen. This includes both the detail view and all the editors. A better solution is to create a custom car detail view for iPad that correctly interacts with the menu.

Car Detail Controller, Take 2: iPad Specific

On an iPhone, the CarValet app is typically used in portrait, so information usually flows from top to bottom. On an iPad, the same is true for the master/menu view of a split view controller, but not for the detail view. The iPad is wider than the iPhone, no matter what the iPad orientation, and this opens up the possibility of using horizontal grouping of data items.

As with any other screen, there are many possible designs, depending on what you are trying to achieve. These are the goals:

Image Visually group related information.

Image Make the most relevant information easy to find.

Image Easily enable both reading and editing.

Image Use a more appropriate presentation of information, where appropriate.

ViewCarTableViewController has lots of wasted space, as you can see from the highlighted areas in Figure 11-13.

Image

Figure 11-13 Ineffective use of iPad screen

There are four basic groups of information: make/model/year, car picture, time parked, and fuel. Because the iPad has a larger screen, you can show a larger picture. For a valet, the picture and time are likely to be important, and fuel is probably the least important for finding the car, though it is very important for returning it.

With most screens, the eye tends to start in the center. Therefore, the picture and time should be vertically centered. Next, the eye tends to do left-to-right scanning, usually from top to bottom. Since make and so on are the next most important items, they should appear at the top of the screen, going from left to right. Finally, fuel is at the bottom.

For the make/model/year items, the larger screen makes it possible to put a label on top and an edit field below. Using a label on the left of a field would either leave far too much whitespace on the right or create a crowded line that is hard to read.

For fuel, you can use a rotating picker similar to those used for dates and times. With this kind of presentation, there is no special parsing required for other locales. The only localization concerns are the separator, the language direction, and the characters for the numbers. You saw how to work with all these in Chapter 5, “Localization.”

The car picture can be vertically centered in the most compact view. If you add the ability to take a picture or check the album, you could enable adding or associating an image by tapping. Time Parked goes on the left of the picture.

When you implement the changes described here, you get a screen that looks like Figure 11-14. The valet tent picture is used as a light background to enable a smooth transition from no car selected to showing car detail. You can try this yourself by hiding the background picture image view in the car detail controller, running the app, and then selecting a car. The transition from a screen with dense content (the empty detail area state showing the valet tent) to an area with lots of white space is visually jarring.

Image

Figure 11-14 Car detail, take 2

Controller Layout on the Storyboard

The first step in building the car detail view controller for the iPad is creation and layout. This involves both adding elements and setting up appropriate auto layout constraints for both landscape and portrait. Generally, this involves three steps. First, put the elements you need in the controller in one orientation. Second, set up the constraints. And third, check the other orientation.

First, put the view elements in the controller roughly where you need them:

1. Open Main_iPad.storyboard and drag in a new UIViewController. With the new controller selected, use the Attributes inspector to set the size simulated metric to Detail and the orientation to Landscape. You can use these simulated metrics to test your layout in both portrait and landscape.

2. Drag a label in so it is the system distance from the top and left. Set the label text to Make and the alignment to centered. Set the font to System Bold 17.

3. Drag a text field and place it the system distance below the Make label and the system distance from the left edge. Use the Attributes inspector to set the text field placeholder to Car Make and set the Clear button to appear while editing.

4. Duplicate the label and edit field pair, and move it so it is centered horizontally in the parent view, with the label still the system distance from the top of the parent. Set the label text to Model and the placeholder to Car Model.

5. Select the new label and field, duplicate them, and move the duplicate pair so the right side is the system distance from the right edge and the label is the system distance from the top. Change the label to Year, the placeholder text to Car Year, and the keyboard type to Number Pad.

6. Drag in a picker view so the left, right, and bottom sides are flush with the parent. Duplicate the Make label, put the system distance on top of the picker and centered in the view. Change the text to Fuel.

7. Place an image view the system distance from the container’s leading edge and the bottom of the Make Text entry field. Align the trailing edge with the trailing edge of the Model Text entry field, and set the bottom edge the system distance above the Fuel label.

Set the background to Sky (to make it easy to see during development; you set it back later).

Import CH11 Assets Big Placeholder into Images.xcassets. Set the picture for the image to big-placeholder and Aspect Fit.

8. Drag a label view so its leading edge is aligned with the leading edge of the Year Text entry view and roughly vertically centered with the picture. Then create two copies of that label, so they are the system distance apart vertically and their leading edges are aligned with the first label.

9. Change the top label to System Bold 17 and change the text to Time Parked. The next label is Day, and the one after is Time. The last two label changes are to help with layout. In the app, those fields are replaced with the date and time that the car records were created.

10. Select all three labels and vertically center the Day label with the car image.

11. Drag in an image view and resize it to fill the entire parent view. Set the image to Detail Default, the mode to Aspect Fill, and the alpha to 0.2. Use the left-hand list of view controllers to move the image view to the background, just below the top-level parent view.

Now it is time to add the constraints. Only update frames when instructed to do so:

1. Select the Make label and set it to the system distance from the leading and top edges of the superview and the system distance from the Model label.

2. Set the Model label to the system distance from the superview’s top and Year label.

3. Set the Year label to the system distance from the superview’s top and trailing edges.

4. The Make text field is system distance from the superview’s leading edge, below the Make label and Model text field.

5. Model and Year text fields are similar—system distance from the label above them, and from their neighboring text fields, or for the Year, the trailing edge.

6. Now select all the labels and text fields, set them to equal widths, and update their frames.

7. Choose the picker and set leading, bottom, and trailing to zero from the container.

8. The Fuel label is system distance from the picker and horizontally centered in the container.

9. The placeholder image has the most constraints and is used to fill space between the top and bottom entry areas. It expands or shrinks as needed:

Image The leading edge is system distance from the superview.

Image The top edge is system distance from the bottom of the car Make text field.

Image The bottom edge is greater than or equal to the default space from the top of the Fuel label.

Image The trailing edge is aligned with the trailing edge of the Model text field but with a priority of High (750).

Image Reduce the Content Compression Resistance priority by 1 for each axis to 749.

10. The remaining three labels also have slightly complex constraints:

Image The Time Parked, Day, and Time labels are the system distance apart, with Time Parked on the top and Time on the bottom.

Image The leading edge of Time Parked is aligned with the leading edge of the Year text field.

Image Day and Time leading edges are aligned with Time Parked.

Image Day is vertically centered with the placeholder image view.

11. The background image is zero distance from the superview for all four edges.

This time, update the frames for the whole view controller. Do this by selecting the view controller (not the view) and choosing Editor > Resolve Auto Layout Issues > Update All Frames in View Controller. A similar choice is available from the resolution popup in the toolbar.

Use the simulated metrics to rotate to landscape. You might see some misplaced frames, but those are actually not an issue. Updating the frames in the controller shows little if any change. Now you can rotate between portrait and landscape and have all the elements align correctly.

Setting Up the Controller

You need a custom class for the new controller so you can populate the user interface elements and work with the picker view. Follow these steps:

1. Use the Navigator to select MainMenuViewController.m and then add a new class called CarDetailViewController that is based on UIViewController.

2. Open the Storyboard editor and set the custom class and storyboard ID of the new controller using the Identity inspector.

3. Set the fuel picker delegate and data source to the car detail view controller. You do this the same way you would for a table view controller: Ctrl-click the picker view and drag connections from the dataSource and delegate outlets to the car detail view controller.

4. Open the Assistant editor and drag connections to set properties as shown in Table 11-5.

Image

Table 11-5 CarDetailViewController Properties

Now switch over to CarDetailViewController.m, and view the .h file in the Assistant editor. Before you can test the look of the new controller, you need to implement the fuel picker data source and delegate protocols. Follow these steps:

1. Forward declare the CDCar class in the .h file by placing the following line of code above the interface declaration:

@class CDCar;

2. Add support for the UIPickerViewDataSource and UIPickerViewDelegate protocols.

3. Declare myCar, a weak, nonatomic property of type CDCar in the .h file.

4. Switch to the .m file and add a line for hiding the Back button to viewDidLoad:

self.navigationItem.hidesBackButton = YES;

5. Add the code in Listing 11-13 for basic support of the fuel picker protocols. Put the code just below the end of didReceiveMemoryWarning.

Listing 11-13 Fuel Picker Support Protocols


#pragma mark - UIPickerView DataSource

- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView { // 1
return 5;
}

- (NSInteger)pickerView:(UIPickerView *)pickerView
numberOfRowsInComponent:(NSInteger)component { // 2
if (component == 3) {
return 1; // 3
}

return 10; // 4
}

#pragma mark - UIPickerView Delegate

- (NSString *)pickerView:(UIPickerView *)pickerView
titleForRow:(NSInteger)row
forComponent:(NSInteger)component { // 5

if (component == 3) {
return @"."; // 6
}

return [NSString stringWithFormat:@"%d", row]; // 7
}


Here’s what happens in the numbered lines in Listing 11-13:

1. Specify a method to return how many components, or “wheels,” are in the picker: 5 for three places before the decimal, one after, and the decimal point.

2. Return the number of rows in a particular component.

3. This is the decimal point wheel, so return 1 for the number of rows.

4. Since the decimal point condition did not fire, this must be a number wheel. Return 10 for the numbers zero to nine.

5. Specify the title to show at a particular row on a given wheel.

6. Show . for the decimal point. Note that in an app with localization, you should use the locale to set the decimal separator. Refer to the “Formatting and Reading Numbers” section in Chapter 5 for how to do this.

7. For the number rows, the row number is the title. Return the row number converted to a string.

Modifying MainMenuViewController

Before you can run the CarValet app, you need to open the new car detail controller with MainMenuViewController. The current main menu controller opens the iPhone-based car detail in selectCar: instead of using tableView:didSelectRowAtIndexPath: to centralize menu selection behavior. To add opening the new detail controller, follow these steps:

1. Remove all but the NSLog statement from selectCar:.

2. Replace the import of ViewCarTableViewController.h with CarDetailViewController.h and then replace the declaration of currentViewCarController with this:

CarDetailViewController *currentCarDetailController;

3. Change the kPadMenuCarsItem case in tableView:DidSelectRowAtIndexPath: to the code in Listing 11-14.

Listing 11-14 Updated kPadMenuCarsItem Case


case kPadMenuCarsItem:
nextController = [iPhoneStory
instantiateViewControllerWithIdentifier:@"CarTableViewController"];

nextController.navigationItem.title = @"Cars";
((CarTableViewController*)nextController).delegate = self;

[self.navigationController pushViewController:nextController
animated:YES];

if (currentCarDetailController == nil) { // 1
currentCarDetailController = [[self storyboard]
instantiateViewControllerWithIdentifier:
@"CarDetailViewController"];
}

nextController = currentCarDetailController; // 2
break;


Here’s what happens in the numbered lines in Listing 11-14:

1. If there is no current detail controller, load one from the iPad storyboard.

2. Set nextController so DetailController can either show the new detail view or leave things if it is the existing detail view.

Now load the selected car into the new car detail by adding one line to the end of selectCar:

currentCarDetailController.myCar = selectedCar;

There is now enough code and IB work to show a car detail view, though not the details of any car. Run the app in the iPad and try both portrait and landscape. Update the constraints if needed to match the layout in Figure 11-14. Confirm that the fuel picker wheels have the correct range of values. Make sure the text fields have the correct keyboards: alphanumeric for the first two and a number pad for the year.

Preparing the Picker

The next step is to set values in and get values from the fuel picker. The picker shows a floating-point number as a series of individual digits. Setting the picker requires extracting the right positional digit for each wheel. Getting the fuel from the picker means compositing the positional digits back together. Follow these steps:

1. Import CDCar.h into CarDetailViewController.m.

2. Set up #defines for each component index by adding this code above the implementation statement:

#define kFuelPickerHundreds 0
#define kFuelPickerTens 1
#define kFuelPickerOnes 2
#define kFuelPickerDecimal 3
#define kFuelPickerTenths 4

3. Add #pragma before the @end:

#pragma mark – Utility Methods

4. Implement getFuelValue. This is the easier method for composing a float by adding each positional value multiplied by the base value of that position. For example, for the second wheel, or tens position, you would take the value of that wheel and multiply by 10. This is the code for that:

- (float) getFuelValue {
float fuel = 0.0;

fuel = [self.fuelPicker selectedRowInComponent:kFuelPickerHundreds]
* 100.0;
fuel += [self.fuelPicker selectedRowInComponent:kFuelPickerTens]
* 10.0;
fuel += [self.fuelPicker selectedRowInComponent:kFuelPickerOnes]
* 1.0;
fuel += [self.fuelPicker selectedRowInComponent:kFuelPickerTenths]
* 0.1;

return fuel;
}

5. Find the wheel values by using the highest remaining positional value and then subtract the result from the existing number. For example, if the fuel is 632.4, the 100s wheel value is the floor of dividing the fuel by 600. To get the 10s (second place) value, subtract 600 (or the 100s value multiplied by 100) and then use the floor of dividing by 10. Add the method in Listing 11-15 to the utility methods area.

Listing 11-15 Setting Fuel Picker Fuel Component Values


- (void)setFuelValues {
float fuel = [self.myCar.fuelAmount floatValue]; // 1

NSInteger currentValue;

currentValue = (NSInteger)floor(fuel / 100); // 2
[self.fuelPicker selectRow:currentValue // 3
inComponent:kFuelPickerHundreds
animated:YES];
fuel -= (currentValue * 100); // 4

currentValue = (NSInteger)floor(fuel / 10); // 5
[self.fuelPicker selectRow:currentValue
inComponent:kFuelPickerTens
animated:YES];
fuel -= (currentValue * 10);


currentValue = (NSInteger)floor(fuel);
[self.fuelPicker selectRow:currentValue
inComponent:kFuelPickerOnes
animated:YES];
fuel -= currentValue;

fuel *= 10; // 6
currentValue = (NSInteger)floor(fuel);
[self.fuelPicker selectRow:currentValue
inComponent:kFuelPickerTenths
animated:YES];
}


Here’s what happens in the numbered lines in Listing 11-15:

1. Get the current fuel level for the car.

2. Starting with the 100s position, divide fuel by 100 and turn the result into an integer with floor.

3. Set the 100s wheel to the integer value.

4. Subtract the 100s value from fuel. For example, if the fuel is 234.6, then step 3 sets the wheel index to 2. This step subtracts 200 from the current fuel, leaving 34.6 as the new value.

5. Use the same approach for the 10s and 1s.

6. To find the tenths, shift the value into the 1s position and use the same method to set the wheel index.

Loading a Car

In the CarValet app, showing the fuel happens when a car is loaded. You also need to set up the other fields and labels, as well as the picture. There is already a public property, myCar, for the car object. The simplest thing to do is to create a setter to update content if there is a new car.

Add the setter from Listing 11-16 just after the class implementation begins.

Listing 11-16 Updating Car Data in the Detail View


#pragma mark - Setters

- (void)setMyCar:(CDCar *)myCar {
if (myCar != _myCar) { // 1
_myCar = myCar; // 2

self.carMakeField.text = _myCar.make; // 3
self.carModelField.text = _myCar.model;
self.carYearField.text = [_myCar.year stringValue];

self.dayParkedLabel.text = [NSDateFormatter // 4
localizedStringFromDate:_myCar.dateCreated
dateStyle:NSDateFormatterMediumStyle
timeStyle:NSDateFormatterNoStyle];

self.timeParkedLabel.text = [NSDateFormatter
localizedStringFromDate:_myCar.dateCreated
dateStyle:NSDateFormatterNoStyle
timeStyle:NSDateFormatterMediumStyle];

[self setFuelValues]; // 5
}
}


Here’s what happens in the numbered lines in Listing 11-16:

1. Check whether the new car is a different object from the current one. This includes setting a car when the current car is nil.

2. Set myCar to the new value. Remember to use the underscore version of a property inside a setter.

3. Set the make and model text fields from the corresponding car properties.

4. Use date formatters to set the time and date labels.

5. Call setFuelValues, the method written above, to set the fuel spinner.

Run the app and try looking at different cars in both portrait and landscape. All values should load correctly, though none save. This is also a good time to change the background of the car image back to the default—that is, clear.

Saving Cars

Several events could trigger saving, including clicking a Save button, changing a field or picker value, changing the car, or closing the car view. Value changes require code to detect them, and a Save button breaks the smooth user experience.

Follow these steps for saving the edited car when a new one is viewed or the controller moves from the detail content:

1. Add the saveCar method at the end of the utilities area:

- (void)saveCar {
self.myCar.make = self.carMakeField.text;
self.myCar.model = self.carModelField.text;
self.myCar.year = @([self.carYearField.text floatValue]);

self.myCar.fuelAmount = @([self getFuelValue]);
}

The year is read from the text using floatValue, and fuel uses getFuelValue. dateCreated does not need to be saved because it is not editable.

2. To take care of saving when cars are changed, call saveCar in setMyCar: if the new car and old car are different:

...
if (myCar != _myCar) {
[self saveCar];

_myCar = myCar;
...

3. To take care of closing the view or moving it offscreen, add viewDidDisappear: under viewDidLoad and call saveCar after invoking the method on the superclass:

- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];

[self saveCar];
}

This time when you run the app, the cars are saved, though the menu of cars does not reflect the change. In this version, updating the table requires viewing a different top-level menu item such as About and then viewing the Car menu.

Polishing the Car Detail Controller

You need to do four things to polish the car detail controller:

Image Update the car table menu when the details change.

Image Close the car detail content controller when dismissing the car table menu.

Image Disable editing in the car detail controller when there is no current car.

Image Change the popover menu behavior to stay open when Cars is selected and closed when a particular car is selected.

Updating a previous car table cell happens in one of three ways: The user selects a new car, goes back to the top-level menu, or changes the car table menu sort. All these actions happen inside CarTableViewController:

1. Open CarTableViewController.m and add the following bold code inside the if condition of tableView:didSelectRowAtIndexPath::

if (self.delegate != nil) {
NSIndexPath *previousPath = currentViewCarPath;

[self.delegate selectCar:[self carToView]];

if (previousPath != nil) {
[currentTableView reloadRowsAtIndexPaths:@[previousPath]
withRowAnimation:NO];
}
}

Ensure that previousPath is not nil, or the reload call crashes.

2. Modify the carSortChanged: method to clear the current car index path for any current delegate. That means checking for a delegate and selected row, and if they both exist, updating the table and telling the delegate there is no selection. Do this by adding the following bold code to the top of the method below the variable declarations:

SEL compareSelector = nil;

if ((self.delegate != nil) &&
(self.tableView.indexPathForSelectedRow != nil)) {
currentViewCarPath = nil;
[self.delegate selectCar:nil];
}

switch (self.carSortControl.selectedSegmentIndex) {

Run the app and confirm that the cell values update as you move between cars, updating fields.

As you gain more experience with iOS, you’ll see that there are better ways to keep multiple objects synchronized with changes in data. (See the “Note: A Better Way to Update.”) However, they are beyond the scope of this book.


Note: A Better Way to Update

The best way to update the car table is by using an iOS notification mechanism. Doing so means every controller or object that cares about a change can register to be notified and take appropriate action. When you use this mechanism, you don’t need to make multiple changes every time you add some code that can modify data.

However, NSNotificationCenter and Key Value Coding/Key Value Observation (KVC/KVO) are more advanced uses of iOS and beyond the scope of this book. With KVC, the consequences of an incorrect implementation are sending messages to at best nil and more usually deallocated objects. These are some of the hardest bugs to track down. For more information, see Objective-C Phrase Book by David Chisnall or the Apple documentation.


Closing the Car Detail Controller

Closing the car detail controller when the user goes back to the main menu requires only a few lines of code in MainMenuViewController. When you do this, you are guaranteed the following:

Image Tapping the Back button opens the main menu.

Image The main menu is the only place that the car table menu is created.

Image The main menu is the only place the car detail controller is created, and it will have a reference to that controller.

You can use one of the view life cycle methods for closing the detail controller. You need a message sent after the initial view is loaded, so it will be one of the Appear messages, either viewWillAppear: or viewDidAppear:. The big difference between the two is when the animation occurs. viewWillAppear: is called before the view appears but after everything is laid out. The other is called after the view is rendered on the screen.

Insert the code in Listing 11-17 in MainMenuViewController.m below viewDidLoad and try it. Then try the difference in timing using viewDidAppear:. You will find that the latter has too much lag time before the animation and is not balanced with originally showing the car detail content.

Listing 11-17 Closing Car Detail Content


- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];

if (currentCarDetailController != nil) { // 1
[DetailController sharedDetailController].currDetailController = nil;
currentCarDetailController = nil; // 2
}
}


Here’s what happens in the numbered lines in Listing 11-17:

1. Update the detail content only if there is a current detail car controller.

2. nil out the local reference to avoid conflicts with the next controller. If you want to see the issue, comment out the line, select the car detail view, select the about view, and then go back to car detail view. The about view still shows.

Again, confirm that the changes work by running the app. It is a good idea to incrementally test changes as soon as possible.

Enabling and Disabling Editing Car Details

Preventing the user from interacting with any editable items is the fastest way to disable editing. Some views can be enabled and disabled, and for any that cannot, you can disable user interaction.

You want to disable editing whenever there is no car to edit, something that can happen in two ways. First, the controller might have just been shown and not had a car set. Second, you can explicitly set myCar to nil. Of course, you need to reenable editing when there is a valid car, and this suggests a method with an argument for the enabled state. Add the method in Listing 11-18 to CarDetailViewController.m below saveCar.

Listing 11-18 Updating Car Detail’s Editable State


- (void)updateEditableState:(BOOL)enabled { // 1
self.carMakeField.enabled = enabled; // 2
self.carModelField.enabled = enabled;
self.carYearField.enabled = enabled;
self.fuelPicker.userInteractionEnabled = enabled; // 3
}


Here’s what happens in the numbered lines in Listing 11-18:

1. Use an argument to enable or disable editing.

2. UITextField has an enabled property.

3. Make sure the fuel picker enables user interaction so users can spin the wheels.

When the car detail controller is first created, it has no car to edit. Add the following bold line to viewDidLoad:

- (void)viewDidLoad
{
[super viewDidLoad];

self.navigationItem.hidesBackButton = YES;
[self updateEditableState:NO];
}

The other time the car detail controller has no car to edit is when the car is changed and the new car is nil. Add the following bold line near the beginning of setMyCar:

...
if (myCar != _myCar) {
[self saveCar];

[self updateEditableState:(myCar != nil)];

_myCar = myCar;
...

Popover Behavior Modifications

The last bit of polish you need to add to the CarValet app at this point is to refine the behavior of the popover menu in portrait mode. Currently, the detail controller is the only object controlling the popover, and it is the only one with a reference. Encapsulation recommends adding a public interface for hiding or showing the popover. Here’s what you do:

1. Open DetailController.m, with the Assistant editor showing the corresponding .h file. Add the following method above the singleton #pragma:

#pragma mark - Public Methods

- (void)hidePopover {
[menuPopoverController dismissPopoverAnimated:YES];
}

2. Modify the declaration of the existing currentDetailController setter so that it looks like this:

- (void)setCurrDetailController:(UIViewController*)currDetailController
hidePopover:(BOOL)hidePopover {

3. Replace the setter’s call to [menuPopoverController dismissPopoverAnimated:YES] with a conditional call to the new method:

...
}

if (hidePopover)
[self hidePopover];

if (newStack != nil) {
...

4. To minimize required changes and maximize compatibility, add code for a new default setter that keeps the old behavior—that is, hiding the popover—and put the new method above setCurrDetailController:hidePopover::

- (void)setCurrDetailController:(UIViewController*)currDetailController {
[self setCurrDetailController:currDetailController hidePopover:YES];
}

5. Add public method declarations for the two new methods to the end of the .h file:

- (void)setCurrDetailController:(UIViewController*)currDetailController
hidePopover:(BOOL)hidePopover;

- (void)hidePopover;

Test the changes and make sure there are no changes in behavior. So far, only the DetailController is calling the new methods, but that is still a good test.

There are two popover behavioral changes to implement. First, the popover should not close after a user taps the top-level Car menu item. Second, the popover should close when the user taps a particular car. MainMenuViewController can handle both of those cases using the methods just added to DetailController.

To keep the popover open, you need to use the new method with the BOOL argument. You also need to prevent the following last call in tableView:didSelectRowAtIndexPath: from setting the current detail controller as that closes the popup:

detailController.currDetailController = nextController;

To add code keeping the popover open, follow these steps:

1. In the tableView:didSelectRowAtIndexPath: method of MainMenuViewController.m, declare a Boolean above the switch statement to indicate whether currDetailController should be updated. This variable can replace newDetail, which is no longer required:

BOOL updateDetailController = YES;

switch (indexPath.row) {

2. Change the last line to conditionally update the detail controller based on the new variable:

if (updateDetailController) {
detailController.currDetailController = nextController;
}

3. Modify the end of the kPadMenuCarsItem case to use the new setter so it looks like the following code. Note that the line to set the nextController to nil is gone. The new code is in bold:

...
if (currentCarDetailController == nil) {
currentCarDetailController = [[self storyboard]
instantiateViewControllerWithIdentifier:
@"CarDetailViewController"];

[detailController setCurrDetailController:currentCarDetailController
hidePopover:NO];
}

updateDetailController = NO;

break;
...

4. In the kPadMenuImagesItem case, remove the line that sets newDetail to NO.

5. Remove any lines of code from between the end of the switch statement and the if statement you modified in step 2.

6. Hide the popover when a car is selected by adding one line of code at the end of selectCar:

[[DetailController sharedDetailController] hidePopover];

Run the CarValet app and try all the combinations: navigating to car detail, trying to edit the blank detail content view, going back to the main menu, going to details, viewing different cars, changing values, making sure cells update when you navigate away, and so on. Also test the app on an iPhone and/or iPod touch since you made changes to a controller from that storyboard.

Summary

You have turned the CarValet app into a universal app, one that supports both iPhone/iPod touch and iPad. By doing so, you greatly increase the number of possible customers. As you went through this chapter, you learned some important information about designing for the iPad, including how to present information and how to use the split view controller for navigation-based apps.

You explored UISplitViewController, including how it uses master and detail controllers to make navigating hierarchies of data easy. You began adding a tablet experience by adding a split view controller to the iPad storyboard and adding code to check whether the app is running on an iPad.

Next, you added navigation to the master controller and got practice with a static table view. You added DetailController to make sure your app works in portrait and landscape. It manages presentation of the master controller in both orientations, including adding or removing a menu button. You learned that you can use singleton managers for many different tasks.

You moved on to implementing the detail screens in three different ways. In one case, you loaded the About screen from an XIB file. In another case, you loaded the car images from the iPhone storyboard, a useful skill for any developer. Finally, you created a targeted car detail screen and learned some design considerations for tablet versus phone screens. You solidified loading from other storyboards by creating an app navigation submenu for viewing a list of cars.

While adding the detail screens, you learned how to create a menu manager for transitioning between app sections. And you used an advanced way of doing view animations to make moving between sections look even better.

As you progressed through the chapter, you learned about singleton objects and why and when you might use them. You got some significant practice with accessor methods, as well as tips on designing classes. You increased your toolkit, adding UIPickerView, which is a new view element, and you learned a more refined way of transitioning between views using CATransition.

Now that you have finished this chapter, you can create apps for iPhone or iPad, or universal apps that work on both. You can now make key design decisions based on screen space. And for iPad, you can use the split view controller for navigation, as well as use your own detail and menu controllers to manage each activity. By using singletons, you can create managers and other kinds of objects where there can be only one instance.

You now have most of the pieces you need to create apps. There is one more piece that is central to the phone and tablet experience though: touch. Except for a few hardware buttons such as volume, almost all interaction and input from a user is by touch. In Chapter 12, “Touch Basics,” you work directly with touches, including detecting system and custom gestures, as well as tracking movement.

Challenges

1. In CarDetailViewController, every time the car is changed, all the data is saved, even if it does not change. Update the controller to save data only when the data has changed.

2. In CarDetailViewController, entering the fuel is easy, but reading it is not. Add a label that shows the fuel and changes when the picker changes. Make sure the fuel area is visually grouped.

3. Add a new top-level menu item called Almost Blank that brings up a view controller with a label that says “This view intentionally left blank (except for this text).”

4. Practice setting an image for a car. This challenge requires the artwork in the CH11 Assets Big Placeholder folder from the sample code for this chapter. The basic steps to do this are as follows:

Image Add a button to the car detail view centered over the car image view.

Image Clear the button title and set the image for the default state to big-placeholder.

Image Then change the State Config to Highlighted and set the image to big-placeholder-selected.

Image Constrain the button appropriately so it shows up correctly.

Image Now add code to detect if the button was pressed, and if so, set the image in the car image view to one of the pictures added in Chapter 6, “Scrolling.” You also need to hide or show either the button or image view based on if a custom image is set.