Now that we’ve discussed the UITableView
and UINavigationController
(as well as their associated classes and views) and built an iPhone application using them, you’ve actually come a long way toward being able to write applications on your own. With these classes under your belt, you have the tools to attack a large slice of the problem space that iOS applications normally address.
In this chapter, we’ll look at some of the other view controllers and classes that will be useful when building your applications: simple two-screen views (utility applications), single-screen tabbed views (tab bar applications), view controllers that take over the whole screen until dismissed (modal view controllers), and a view controller for selecting video and images (image picker view controller). We’ll also take a look at the Master-Detail Application template and see how it is implemented differently on the iPhone (using a UINavigationController
) than on the iPad (using a UISplitViewController
) along wit the iPad-only Popover controller.
Utility applications perform simple tasks: they have a one-page main view and another window that is brought into view with a flip animation. Both the Stocks and Weather applications that ship with the iPhone are examples of applications that use this pattern. Both are optimized for simple tasks that require the absolute minimum of user interaction. Such applications are usually designed to display a simple list in the main view, with preferences and option settings on the flip view. You access the flip view by clicking a small i icon from the main view.
The Xcode Utility Application template implements the main view and gives the user access to a flipside view. It is one of the most extensive templates in Xcode and it implements a fully working utility application, which is fortunate, as the documentation Apple provides regarding this type of application is otherwise somewhat lacking in details.
Open Xcode and start a new project. Click Application under the iOS group, and then select Utility Application from the New Project window as the template (see Figure 6-1). Click Next, and enter BatteryMonitor
when asked for the Product Name, and BM
when asked for the class prefix for this project. We want an ARC-based template, but don’t want Storyboards, Core Data support, or Unit Tests.
The names of the classes the Xcode template generates are meant to hint strongly at what each of them does, and since the template implements all the logic necessary to control the application’s interface, we only need to implement our own user interface and some basic logic to control it.
Click the Run button in the Xcode toolbar to compile and run the application. You’ll find that it’s a fully working utility application, although with blank main and flipside views (see Figure 6-2).
Figure 6-2. The basic Utility Application running in the iPhone Simulator, showing the front side (left) and flipside (right) views
The somewhat descriptive name of the application has probably revealed its purpose already. We’re going to implement a simple battery monitoring application, and to do so, I’m going to introduce you to the UIDevice
class. This is a singleton class that provides information relating to your hardware device.
A singleton class is restricted so that only one instance of the class can be created. This design pattern can be used to coordinate actions or information across your application. Although some argue that because use of singleton classes introduces global state into your application, and is therefore almost by definition a bad thing, I think that when it is used correctly, the pattern can simplify your architecture considerably.
From it you can obtain information about your device such as its assigned name, device model, and operating system name and version. More important, perhaps, you can use the class to detect changes in the device’s characteristics, such as physical orientation, and register for notifications about when these characteristics change.
The UIDevice
class used to return the UDID of the iOS device your application is running on, and the developer community has extensively used this ability in the past to obtain a per-device unique identifier. However, with the arrival of iOS 5, this capability was deprecated, and with the arrival of iOS 6 this capability went away.
Information—and notifications—about the device battery state weren’t introduced until the 3.0 update of the SDK. Even now, the implementation is somewhat coarse-grained (notifications regarding charge level changes occur in only 5% increments).
The UIDevice
class has several limitations, and some developers have resorted to the underlying IOKit framework to obtain more information about the device (e.g., better precision to your battery measurements). However, while Apple marked the IOKit as a public framework, no documentation or header files are associated with it.
If you use this framework and try to publish your application on the App Store, it is possible that Apple will reject it for using a private framework despite its apparent public status. In the official documentation, IOKit is described as “Contain[ing] interfaces used by the device. Do not include this framework directly.”
First we’re going to build the interface. Double-click on the BMMainViewController.xib file to open it in Interface Builder. You’ll see that the default view that Xcode generated already has the Info button to switch between the main and flipside views, and not only is it there, but it’s connected to the template code so it’s already working.
The UI will consist of just three UILabel
elements, so drag and drop three labels from the Object Library onto the view, and position them roughly as shown in Figure 6-3.
You can use the Attributes Inspector in the Utilities panel to change the font size and color as I have done with my view. We’ll be setting the text of the labels from inside our code, but for now I’ve added placeholder text (100%, State:, and Unknown) using the Attributes Inspector so that I can position the labels more neatly and get a better idea of how my interface will look.
Let’s connect our interface to our code. Select the Assistant Editor to open up the interface file associated with our nib file. If you’re short on screen real estate, you may want to minimize the dock and close the Utilities panel at this point.
Then Control-click and drag from the 100% label into your code (just above the existing IBAction
, which was connected to the Information button in the bottom-right of the view by the template) and name your outlet levelLabel
. Next, Control-click and drag from the state label (which has the placeholder text Unknown) into the code and name your outlet stateLabel
, as in Figure 6-4.
Afterward, your interface file should look like this:
#import "BMFlipsideViewController.h" @interface BMMainViewController : UIViewController <BMFlipsideViewControllerDelegate> @property (weak, nonatomic) IBOutlet UILabel *levelLabel; @property (weak, nonatomic) IBOutlet UILabel *stateLabel; - (IBAction)showInfo:(id)sender; @end
That’s all we’re going to do to the main view. Save the nib file and open the BMFlipside__ViewController.xib file. You’ll see that this time, the default view that Xcode generates already has a navigation bar and a Done button present and connected to the template code. Switch back to the Standard Editor and reopen the Utilities panel. You’ll need to add a label (UILabel
) and switch (UISwitch
) to this interface.
Drag and drop the two elements from the Object Library into the Flipside View window and position them as shown in Figure 6-5. Set the text of the label to Monitor Battery
, and use the Attributes Inspector to set the label text color to white. The default black text won’t show up well against the dark gray background of the view.
We now need to connect our switch back into our code. Reopen the Assistant Editor and Control-click and drag from the switch in the view to the associated interface file to create an outlet (see Figure 6-6). Call the outlet toggleSwitch
when asked.
Afterward, your interface file should look like this:
#import <UIKit/UIKit.h>
@class BMFlipsideViewController;
@protocol BMFlipsideViewControllerDelegate
- (void)flipsideViewControllerDidFinish:(BMFlipsideViewController *)controller;
@end
@interface BMFlipsideViewController : UIViewController
@property (weak, nonatomic) IBOutlet id <BMFlipsideViewControllerDelegate>
delegate;
@property (weak, nonatomic) IBOutlet UISwitch *toggleSwitch;
- (IBAction)done:(id)sender;
@end
We’re going to use the switch to turn battery monitoring on and off.
Open up the BMAppDelegate.h interface file. We’re going to need to add a Boolean variable that stores the flag that indicates whether the app is currently monitoring the battery state. Add the following property next to the existing @property
declarations (but before the @end
):
@property (nonatomic) BOOL monitorBattery;
By default, we’re going to make it so that the application starts with battery monitoring turned off, so in the application:didFinishLaunchingWithOptions:
method, we must set the flag to NO
. Add the following to the top of the method:
self.monitorBattery = NO;
Note that we access the variable by using the accessor
method that was automatically generated for us. It’s important to
realize that accessing the underlying instance variable directly using
_monitorBattery
and accessing
the property via a call to self.monitorBattery:
are completely
different in Objective-C, since you are sending a message when you
invoke the property, rather than directly accessing the
variable.
Next, open the BMFlipsideViewController.m implementation file. You first need to import the application delegate header file, as we’ll need access to the Boolean property we added. Add the following line to the top of BMFlipsideViewController.m:
#import "BMAppDelegate.h"
Next, make the changes shown in bold to the viewDidLoad:
method:
- (void)viewDidLoad { [super viewDidLoad]; self.title = @"Preferences"; BMAppDelegate *appDelegate = (BMAppDelegate *) [[UIApplication sharedApplication] delegate]; self.toggleSwitch.on = appDelegate.monitorBattery; }
Now modify the done:
method to save the status of the toggle switch back to the application delegate when you close the flipside view:
- (IBAction)done { BMAppDelegate *appDelegate = (BMAppDelegate *) [[UIApplication sharedApplication] delegate]; appDelegate.monitorBattery = self.toggleSwitch.on; [self.delegate flipsideViewControllerDidFinish:self]; }
The modifications we need to make to the main view controller are a bit more extensive than those we’ve made thus far. Open the BMMainViewController.h interface file and make the changes shown in bold.
#import "BMFlipsideViewController.h" @interface BMMainViewController : UIViewController <BMFlipsideViewControllerDelegate> @property (weak, nonatomic) IBOutlet UILabel *levelLabel; @property (weak, nonatomic) IBOutlet UILabel *stateLabel; - (IBAction)showInfo:(id)sender; - (void)batteryChanged:(NSNotification *)note; - (NSString *)batteryLevel; - (NSString *)batteryState:(UIDeviceBatteryState )batteryState; @end
This method will be called when we receive a notification that there has been a change in the state of the battery.
This is a convenience method to wrap the call to UIDevice
to query the current battery
level and return an NSString
that we can use for the text of one of the UILabel
s.
This is another convenience method to convert a UIDeviceBatteryState
into an NSString
that we can use for the text of
one of the other UILabel
s.
Save the interface file, and then open the BMMainViewController.m implementation file in Xcode. We’ll need to reference the application delegate in the interface file, so now we need to import the relevant header file. Add this line at the top:
#import "BMAppDelegate.h"
You should see a small warning triangle next to the @implementation
line in the BMMainViewController.m implementation file in the Xcode Editor. This is indicating that we’ve added methods to the interface file that we haven’t yet implemented; it’s safe to ignore, as we’re about to go ahead and do that.
Next, we need to implement the viewWillAppear:
method, another UIApplicationDelegate
callback method which will be called before the existing viewDidLoad:
method.
At this point, you may be wondering what the difference is between this method and the previous viewDidLoad:
method. The answer is that they’re called at different times: viewWillAppear:
will be called each time the view becomes visible, while viewDidLoad:
is called only when the view is first loaded. Because the changes we make to the preferences (on the flip side) affect the main view, we need to use viewWillAppear:
, which is triggered each time we flip back from the preferences view to the main view. Add the following to BMMainViewController.m:
- (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; UIDevice *device = [UIDevice currentDevice]; BMAppDelegate *appDelegate = (BMAppDelegate *) [[UIApplication sharedApplication] delegate]; device.batteryMonitoringEnabled = appDelegate.monitorBattery; if (device.batteryMonitoringEnabled) { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(batteryChanged:) name:@"UIDeviceBatteryLevelDidChangeNotification" object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(batteryChanged:) name:@"UIDeviceBatteryStateDidChangeNotification" object:nil]; } else { [[NSNotificationCenter defaultCenter] removeObserver:self name:@"UIDeviceBatteryLevelDidChangeNotification" object:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self name:@"UIDeviceBatteryStateDidChangeNotification" object:nil]; } self.levelLabel.text = [self batteryLevel]; self.stateLabel.text = [self batteryState:device.batteryState]; [super viewWillAppear:animated]; }
This sets the current battery monitoring state in the singleton UIDevice
object to correspond to our current battery monitoring state, as determined by the switch on the flipside view.
If battery monitoring is enabled, we’re going to add our object as an observer to receive notifications when either the battery level or the battery state changes. If either of these events occurs, the batteryChanged:
method will be called.
If battery monitoring is disabled, we’re going to remove the object as an observer for these notifications.
In either case, we’ll populate the text of our two UILabels using the convenience methods (batteryState:
and batteryLevel:
, which we’ll define shortly).
Since the object may be registered as an observer when we deallocate this view, we also need to make sure we remove ourselves as an observer of any notifications in the dealloc
method (see Chapter 4 for a discussion of the dealloc
method). Add the method below to your code:
- (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; }
We also need to implement the batteryChanged:
method, which is called when our application is notified of a change in battery state. Here, all we’re doing is updating the text of our two labels when we receive a notification of a change. Add the following to BMMainViewController.m:
- (void)batteryChanged:(NSNotification *)note { UIDevice *device = [UIDevice currentDevice]; self.levelLabel.text = [self batteryLevel]; self.stateLabel.text = [self batteryState:device.batteryState]; }
Finally, we need to implement those convenience methods. Add the following to MainViewController.m:
- (NSString *)batteryLevel { UIDevice *device = [UIDevice currentDevice]; NSString *levelString = nil; float level = device.batteryLevel; if ( level == −1 ) { levelString = @"---%"; } else { int percent = (int) (level * 100); levelString = [NSString stringWithFormat:@"%i%%", percent]; } return levelString; } - (NSString *)batteryState:(UIDeviceBatteryState )batteryState { NSString *state = nil; switch (batteryState) { case UIDeviceBatteryStateUnknown: state = @"Unknown"; break; case UIDeviceBatteryStateUnplugged: state = @"Unplugged"; break; case UIDeviceBatteryStateCharging: state = @"Charging"; break; case UIDeviceBatteryStateFull: state = @"Full"; break; default: state = @"Undefined"; break; } return state; }
At this point, we’re done. We’ve implemented everything we need to in code, and we’ve linked all of our outlets to our interface. Unfortunately, since this application makes use of the UIDevice
battery monitoring API, and iPhone Simulator doesn’t have a battery, we have to test it directly on the device.
Make sure your iPhone is plugged in and registered with Xcode, and then change the scheme in the Xcode toolbar from iPhone Simulator to your device, as shown in Figure 6-7.
Now click the Run button in the Xcode toolbar to build and deploy your application onto your iPhone. The application should automatically start and you should see something a lot like Figure 6-8.
Click the Info button in the bottom righthand corner to switch to the flipside and enable battery monitoring in the Preferences pane. Click the Done button and return to the main view. Both the battery level and the state should have changed. While the battery level only changes every 5%, you can get some immediate feedback by plugging and unplugging your device from your Mac. The state should change from Full or Charging (see Figure 6-8) to Unplugged after you unplug it from your Mac.
If you need to provide a number of different views on the same data set, or separately present a number of different tasks relating to your application, Apple recommends using a tab bar application. Both the iTunes and the App Store applications that ship with iOS devices are examples of applications that use this pattern.
To create a tab bar application, open Xcode and start a new project. Select the Tabbed Application template from the New Project window (see Figure 6-9). When prompted, ensure the checkbox to use ARC is ticked, while the checkboxes to use storyboarding and unit tests are not. Name the project TabExample
, and use the prefix TE
. We’re going to build a project for the iPhone once again, and we probably don’t need a Git repository.
The default template provides a tab bar application with the two tab items and, at least in the more recent version of Xcode, the template it uses to do so manages the tabbed views in code directly from the application delegate.
There are actually several different approaches you could take to building a tab bar application: creating and managing the tab bar directly from the application delegate, managing the tab’s view entirely from a view controller, loading a individual tab’s view from a secondary nib, or using a hybrid of these three approaches. The one used by Xcode is probably the simplest.
If you are using an older version of Xcode, the tabbed application template may well be very different and could be using a hybrid approach to creating the tabbed views.
If you click the Run button in the Xcode toolbar, you should see something much like Figure 6-10 in the iPhone Simulator. If you tap the two tab bar items, you’ll switch between the two tab views. Each of these views is a separate view controller.
Let’s add another tab bar item so that you can see how to create one from scratch. The first thing we need to do is create a new view controller. Right-click on the main TabExample
group and select New Files from the menu, choose a UIViewController
subclass from the template drop-down menu, and name the new class TEThirdViewController
when prompted by Xcode. Make sure the “With XIB for User Interface” checkbox is ticked. Three files will be created: an interface, implementation, and a corresponding nib file.
Open up the TEThirdViewController.m implementation file, and in the initWithNibName:bundle:
method, add the following code:
- (id)initWithNibName:(NSString *)nibNameOrNil bundle: (NSBundle *)nibBundleOrNil { self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; if (self) { self.title = NSLocalizedString(@"Third", @"Third"); self.tabBarItem.image = [UIImage imageNamed:@"second"]; } return self; }
Although currently our tab bar item doesn’t have its own image, we could easily add one. Just drag and drop the image you want to use into the project in the same way you added the images for the City Guide application in Chapter 5.
Applications need to be prepared to run on devices with different screen resolutions. To support devices with a Retina display, you should provide a high-resolution image for each image resource in your application bundle. The UIImage
class will handle loading the high-resolution version of your image behind the scenes. When creating objects, you use the same filename to request both the standard and the high-resolution versions of your image. For instance:
self.tabBarItem.image = [UIImage mageNamed:@"second"];
On devices with a Retina display, the larger second@2x.png image will be used in place of the standard sized second.png image (see Chapter 12 for more details).
To look like Apple’s icons, your images cannot be larger than 32×32 points in size and they must have a transparent background. I’ve found that PNG images between 20 and 30 points work well as tab bar icons.
You should be aware there is a difference between points and the underlying pixels on a device. One point does not necessarily correspond to one pixel on the screen. The purpose of using points is to provide a consistent size of output that is device independent. How points are actually mapped to pixels is a detail that is handled by the system. In the case of the high-resolution images you supply to your application for use in devices with a Retina display, the high-resolution image is twice the size, but since for a Retina display 4 pixels (2 ×2) equal 1 point, in your application, everything is laid out the same.
One you’ve done that, go back and open the TEAppDelegate.m application delegate file in the editor and import the newly created interface file:
#import "TEThirdViewController.h"
Then, in the application:didFinishLaunchingWithOptions:
method, modify the method as follows:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; // Override point for customization after application launch. UIViewController *viewController1 = [[TEFirstViewController alloc] initWithNibName:@"TEFirstViewController" bundle:nil]; UIViewController *viewController2 = [[TESecondViewController alloc] initWithNibName:@"TESecondViewController" bundle:nil]; UIViewController *viewController3 = [[TEThirdViewController alloc] initWithNibName:@"TEThirdViewController" bundle:nil]; self.tabBarController = [[UITabBarController alloc] init]; self.tabBarController.viewControllers = self.tabBarController.viewControllers = @[viewController1, viewController2, viewController3]; self.window.rootViewController = self.tabBarController; [self.window makeKeyAndVisible]; return YES; }
Here we’re using the new Objective-C literal support introduced with the arrival of iOS6 and Xcode 4.5. Before the arrival of support for literals you’d have had to do something like self.tabBarController.viewControllers = [NSArray arrayWithObjects:viewController1, viewController2, viewController3, nil];
instead.
Finally, edit the three nib files— TEFirstViewController.xib, TESecondViewController.xib, and TEThirdViewController.xib —and add large (say, in 144-pt font) labels saying 1, 2, and 3 to the respective views. This way you can confirm that the correct one is being activated. When you edit your new view controller, TEThirdViewController.xib, you’ll need to add a bottom bar to the view from the Simulated Metrics section—see Figure 6-11.
Make sure you save your changes and then click on the Run button in the Xcode toolbar to compile, deploy, and run the application in iPhone Simulator, as shown in Figure 6-12.
Despite the fact that we haven’t written more than a couple of lines of code in this section, you should now have a working, if rather basic, tab bar application.
Although we haven’t walked through the process of building a full-blown application, you should have begun to see the commonalities and familiar patterns emerging in this application. Our application has an application delegate along with three custom view controllers with which to manage the views. This is a very similar arrangement to both the table view application we wrote in Chapter 5 and the utility application we wrote earlier in this chapter.
At this point, you may want to try building your own application on top of the infrastructure we have created so far. Start with something simple where changing something in one view affects the contents of another view. Don’t worry; take your time, and I’ll be here when you get back.
It’s possible to combine different types of view controllers into one application; one fairly common pattern is for each view of a tabbed application to be managed by a navigation controller. So let’s take our example from the preceding section and add a navigation controller.
First, we’ll modify the TEFirstViewController.xib by adding a table view. Click on the nib file to open it in Interface Builder, then drag and drop a table view from the Object Library into your view, and then, in the Attributes Inspector of the Utilities panel, add a simulated navigation bar from the Simulated Metrics section (see Figure 6-13).
Then switch to the Assistant Editor and Control-click and drag from the table view to the associated interface file to declare the table view as a property (see Figure 6-14) called tableView
.
Finally, Control-click and drag from the UITableView
in the central View panel to the icon representing File’s Owner (the top icon in the dock that looks like a transparent cube) and release the mouse button. A small black pop-up window will appear, as shown in Figure 6-15.
Click on dataSource
to connect the table view to the File’s Owner (the TEFirstViewController
class) as its data source. Right-click and drag again, this time clicking on delegate
in the pop-up window to make the File’s Owner class the table view’s delegate class.
In the Assistant Editor, modify the TEFirstViewController.h interface file to reflect these connections and declare that the class implements both the UITableViewDataSource
and the UITableViewDelegate
protocols.
Once you’ve done this, the TEFirstViewController.h file should look like this:
#import <UIKit/UIKit.h> @interface TEFirstViewController : UIViewController <UITableViewDelegate, UITableViewDataSource> @property (weak, nonatomic) IBOutlet UITableView *tableView; @end
What we’ve done here is indicate that the TEFirstViewController
class both provides the data to populate the table view and handles events generated by user interaction with the table view.
Drop back into the Standard Editor and click on the TEFirstViewController.m implementation file. Having declared the class as a data source and delegate, we need to implement the mandatory methods. Add the following methods to the class:
#pragma mark - UITableViewDataSource Methods - (UITableViewCell *)tableView:(UITableView *)tv cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tv dequeueReusableCellWithIdentifier:@"cell"]; if( nil == cell ) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"]; } cell.textLabel.text = @"Test"; return cell; } - (NSInteger)tableView:(UITableView *)tv numberOfRowsInSection: (NSInteger)section { return 3; } #pragma mark - UITableViewDelegate Methods - (void)tableView:(UITableView *)tv didSelectRowAtIndexPath:(NSIndexPath *)indexPath { [tv deselectRowAtIndexPath:indexPath animated:YES]; }
Now that we’ve prepared the way, let’s create the navigation controller. In the TEAppDelegate.h interface file, declare the property:
@property (strong, nonatomic) UINavigationController *navController;
Finally, in the application:didFinishLaunchingWithOptions:
method, you should make the following changes:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; // Override point for customization after application launch. UIViewController *viewController1 = [[TEFirstViewController alloc] initWithNibName:@"TEFirstViewController" bundle:nil]; UIViewController *viewController2 = [[TESecondViewController alloc] initWithNibName:@"TESecondViewController" bundle:nil]; UIViewController *viewController3 = [[TEThirdViewController alloc] initWithNibName:@"TEThirdViewController" bundle:nil]; self.navController = [[UINavigationController alloc] initWithRootViewController:viewController1]; self.navController.navigationBar.barStyle = UIBarStyleBlack; self.tabBarController = [[UITabBarController alloc] init]; self.tabBarController.viewControllers = @[self.navController, viewController2, viewController3]; self.window.rootViewController = self.tabBarController; [self.window makeKeyAndVisible]; return YES; }
We’re going to set the navigation bar to have a black color, rather than the default blue, as this complements the default style of the tab bar controller.
Here we replace the reference to viewController1
with the navigation
controller that is now managing that view controller and other
associated views.
After doing that, we’ve reached a good place to test the code. Save your changes and click on the Run button in the Xcode toolbar to build and deploy the application into the iPhone Simulator; you should see something much like Figure 6-16.
Of course if you tap on one of the table view cells, nothing, at least at the moment, is going to happen. So from here we just need to add another new view controller, fairly simple in this case, which can then be presented by the navigation controller when a cell is tapped.
Right-click on the TabExample
group and select NewFile. Choose to create a UIViewController
subclass named SimpleViewController
. When asked, make sure the checkbox to create an associated nib file is ticked (see Figure 6-17).
After creation, we’re going to make just one change to the template class. In the viewDidLoad:
method of the newly created SimpleViewController.m implementation file, add the following line:
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"Test";
}
Then, returning to the TEFirstViewController.m implementation file once again, import both the SimpleView
and TEAppDelegate
classes:
#import "TEAppDelegate.h" #import "SimpleViewController.h"
In the tableView:didSelectRowAtIndexPath:
method, the delegate method that is called when a table view cell is tapped, add the following lines:
- (void)tableView:(UITableView *)tv didSelectRowAtIndexPath: (NSIndexPath *)indexPath { TEAppDelegate *delegate = (TEAppDelegate *)[[UIApplication sharedApplication] delegate]; UIViewController *controller = [[SimpleViewController alloc] initWithNibName:@"SimpleViewController" bundle:nil]; [delegate.navController pushViewController:controller animated:YES]; [tv deselectRowAtIndexPath:indexPath animated:YES]; }
Once you’ve done so, save your changes and click the Run button in the Xcode toolbar to build and deploy your application back into the iPhone Simulator once again.
When the application starts, the initial tab view will look the same; however, if you now tap on a table view cell, you’ll be presented with a further view (see Figure 6-18).
If you tap on the second or third tabs, you’ll be switched to that tab without disturbing the status of the first tab. Returning to the first tab by tapping on the relevant tab bar icon for the first tab will return you to the navigation controller in whatever state you left it when you tapped on one of the other tabs.
You’ve now successfully embedded a navigation controller into one of the tabs in the tab view controller; you could go on from this starting point and follow this pattern to embed a navigation controller into each of your tabs.
So far in this chapter we’ve looked at two of Apple’s application templates. However, in this section we’re going to focus once again on an individual view controller—or rather, a way to present a view controller to the user. After table views and the UINavigationController
, it’s probably one of the most heavily used ways to present data: the modal view controller.
You’ll have seen a modal controller in action many times when using your iPhone. A view slides in from the bottom of the screen and is usually dismissed with a Done button at the top of the screen. When dismissed, it slides back down the screen, disappearing at the bottom.
In the main controller, we would generally have a button or other UI element; tapping this would trigger an event linked to the following method in the view controller, which would bring up the modal view:
-(void)openNewController:(id)sender { OtherController *other = [[OtherController alloc] init]; [self presentModalViewController:other animated:YES]; }
In the modal view itself, we would implement a button or some other way to close the view, which would call this method in the view controller:
-(void)doneWithController:(id)sender { [self dismissModalViewControllerAnimated:YES]; }
This dismisses the current modal view.
The best way to explain the modal view is to show it in action. For that, we’re going to go back to the City Guide application we built in Chapter 5. We’re going to make some fairly extensive changes to it, so you should make a copy of the project first and work with the copy while you make your modifications. In this section, I’ll show you how to take your code apart and put it back together again in an organized fashion. This occurs a lot when writing applications, especially for clients who have a tendency to change their minds about what they want out of the application in the first place.
Open the Finder and navigate to the location where you saved the CityGuide project; see Figure 6-19.
Right-click or Control-click on the folder containing the project files and select Duplicate. A folder called CityGuide copy will be created containing a duplicate of our project. You should probably rename it to something more sensible. I suggest CityGuide2. Now open the new version of the project in Xcode and mouse over the blue Project icon at the top of the Project navigator and hit the Enter key, which will make the project name editable.
Enter CityGuide2
as the new project name, and a drop-down will appear (see Figure 6-20) prompting you to approve the changes to the project. Click on Rename when prompted to do so to rename the project.
Xcode may prompt you as to whether you want to create a snapshot of the project before making a mass editing operation. While that’s useful in some cases, there isn’t really any need here. You can just hit “Disable.”
In Chapter 5, we built an application that lets users both add and delete city entries in our table view. Incorporating the functionality to delete table view cells was fairly simple; the complicated part was allowing the ability to add cities. So, let’s take a step back and look at another way to implement that functionality.
First we’re going to go into the CGViewController
implementation and back out of the changes that allowed users to edit the table view. We’re going to replace the Edit button and the associated implementation with an Add button, reusing the AddCityController
code and associated view, but presenting the Add City view modally instead of using the navigation controller.
You may wonder about deleting lots of perfectly good code, but refactoring functionality like this is a fairly common task when you change your mind about how you want to present information to the user, or if the requirements driving the project change. This is good practice for you.
If you want to do a global find (and replace) over the entire project for a word or phrase, you can do so from the Search navigator tab of the lefthand pane in the Xcode window.
To remove functionality like this, first you need to figure out what needs to be removed. If you don’t know the author of the original application, this can sometimes be difficult. Do a project-wide search for “editing,” as shown in Figure 6-21. If you do that, you’ll see that the only mention of “editing” is in the CGViewController.m file. The changes we’ll need to make are fairly tightly constrained inside a single class. We’ll have to make some minor changes elsewhere in the project. Limiting the scope of necessary changes when refactoring code in this way is one of the main benefits of writing code in an object-oriented manner.
Open the CGViewController.m file in Xcode. Begin the refactoring by deleting the following methods in their entirety:
setEditing:animated:
tableView:commitEditingStyle:forRowAtIndexPath:
tableView:editingStyleForRowAtIndexPath:
Remember that the methods as they appear in the file have longer, more complicated names. For example, setEditing:animated:
has a prototype of (void)setEditing:(BOOL)editing
animated:(BOOL)animated
.
Next, do the following:
viewDidLoad:
method, remove the line that adds the self.editButtonItem
to the navigation bar.
tableView:cellForRowAtIndexPath:
method, remove the section enclosed in the if(
self.editing ) { … }
conditional statement, and the else { … }
statement that adds the “Add New City” cell. Additionally, you should remove the line that sets the editingAccessoryType
inside the conditional statement.
if(
self.editing ) { … }
conditional statement in the tableView:numberOfRowsInSection:
method.
tableView:didSelectRowAtIndexPath:
method, remove the &&
!self.editing
expression from the first if
block. Remove the second if
block (which deals with what happens if we are editing) in its entirety.
We’re done. If you do a global search in the project for “editing” you should now come up blank, and the class should appear as shown here:
#import "CGViewController.h" #import "CGAppDelegate.h" #import "City.h" #import "CityController.h" #import "AddCityController.h" @interface CGViewController () @end @implementation CGViewController - (void)viewDidLoad { [super viewDidLoad]; self.title = @"City Guide"; CGAppDelegate *delegate = (CGAppDelegate *)[[UIApplication sharedApplication] delegate]; cities = delegate.cities; } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } #pragma mark UITableViewDataSource Methods - (UITableViewCell *)tableView:(UITableView *)tv cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tv dequeueReusableCellWithIdentifier:@"cell"]; if( nil == cell ) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"]; } NSLog( @"indexPath.row = %d, cities.count = %d", indexPath.row, cities.count ); if (indexPath.row < cities.count ) { City *thisCity = [cities objectAtIndex:indexPath.row]; cell.textLabel.text = thisCity.cityName; cell.textLabel.textColor = [UIColor blackColor]; } return cell; } - (NSInteger)tableView:(UITableView *)tv numberOfRowsInSection: (NSInteger)section { NSInteger count = cities.count; return count; } #pragma mark UITableViewDelegate Methods - (void) tableView:(UITableView *)tv didSelectRowAtIndexPath:(NSIndexPath *)indexPath { CGAppDelegate *delegate = (CGAppDelegate *)[[UIApplication sharedApplication] delegate]; NSLog(@"index.row = %d", indexPath.row); if (indexPath.row < cities.count ) { CityController *city = [[CityController alloc] initWithIndexPath:indexPath]; [delegate.navController pushViewController:city animated:YES]; } [tv deselectRowAtIndexPath:indexPath animated:YES]; } @end
Since you’ve now made fairly extensive changes to the view controller, you should test it to see if things are still working. Click the Build and Run button on the Xcode toolbar, and if all is well, you should see something very similar to Figure 6-22 (and in fact, looking a lot like it did back in Figure 5-14). Tapping on one of the city names should take you to its city page as before.
We’ve deleted a lot of code, so let’s write some more. In the viewDidLoad:
method, we need to replace the Edit button that we deleted with an Add button.
Let’s add a button of style UIBarButtonSystemItemAdd
and set things up so that when it is clicked it will call the addCity:
method in this class. Add the following code to the viewDidLoad:
method:
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(addCity:)];
Since there isn’t an addCity:
method right now, we need to declare it in the CGViewController.h interface file. Open that file, and add this line after the @property
declaration but before the @end
directive:
- (void)addCity:(id)sender;
Now add the implementation to the CGViewController.m file:
- (void)addCity:(id)sender { AddCityController *addCity = [[AddCityController alloc] init]; [self presentViewController:addCity animated:YES completion:NULL]; }
We can pass a block to the presentViewController:animated:completion:
method that will be called when the view controller has been successfully presented to the user, but we don’t really have any use so in this case we’re simply passing NULL
as a parameter. On iPhone and iPod touch, modal view controllers are always presented full-screen, but on iPad there are several different presentation options governed by the modalTransitionStyle
property of the view controller.
This looks almost identical to the snippet of code I showed you at the beginning of this section, but the modal view we’re going to display is the one managed by our AddCityController
class.
Now we need to make a couple of small changes to our AddCityController
class. Open the AddCityController.h interface file in Xcode and declare the saveCity:
method as an IBAction
. Add this line after the @interface { … }
statement but before the @end
directive:
- (IBAction)saveCity:(id)sender;
Open the implementation file (AddCityController.m), and remove the last line (where we pop the view controller off the navigation controller) and replace it with a line dismissing the modal view controller. You’ll also change the return value of the saveCity:
method from void
to IBAction
here, just as you did in the interface file:
- (IBAction)saveCity:(id)sender { CityGuideDelegate *delegate = (CityGuideDelegate *)[[UIApplication sharedApplication] delegate]; NSMutableArray *cities = delegate.cities; UITextField *nameEntry = (UITextField *)[nameCell viewWithTag:777]; UITextView *descriptionEntry = (UITextView *)[descriptionCell viewWithTag:777]; if ( nameEntry.text.length > 0 ) { City *newCity = [[City alloc] init]; newCity.cityName = nameEntry.text; newCity.cityDescription = descriptionEntry.text; [cities addObject:newCity]; RootController *viewController = delegate.viewController; [viewController.tableView reloadData]; } [self dismissViewControllerAnimated:YES completion:NULL]; }
We’re pretty much there at this point; however, before we finish with our changes here, we also need to go up to the viewDidLoad:
method and delete the lines where we add the Save button to the view (it’s a single statement beginning with self.navigationItem.rightBarButtonItem
that spans multiple lines).
Make sure you save the changes you made to the AddCityController
class, and open the AddCityController.xib file inside Interface Builder.
First, drag and drop into the view a navigation bar (UINavigationBar
) from the Object Library. Position it at the top of the view, and resize the table view so that it fits in the remaining space. While you’re there, change the title of the navigation bar from “title” to Add New City.
Next, drag and drop a bar button item (UIBarButtonItem
) onto the navigation bar and position it to the left of the title. In the Attributes Inspector, change the Identifier from Custom to Done. You’ll see that this changes both the text and the style of the button, as in Figure 6-23.
Finally, right-click and drag from the Done button to File’s Owner and connect the button to the saveCity:
received action in our view controller, as in Figure 6-24.
Save your changes to the nib file, as we’ve now finished refactoring our City Guide application.
Click the Run button on the Xcode toolbar to compile and start the application in iPhone Simulator. When the application starts, you should see something like Figure 6-25. Clicking the Add button in the navigation bar should bring up our Add City view; when it does, enter some information and click the Done button. You should see your test city appear in the main table view.
Figure 6-25. The new City Guide application with the Add button on the right of the navigation bar (left) and the modified Add City view (right)
Well done. We’ve just taken the City Guide application apart, put it back together again, and made it work slightly differently. But what if you disliked the way we implemented the ability to add cities in the first version of the application, preferring this approach, but you still want to retain the ability to delete cities? You could still implement things so that a left-to-right swipe brought up the Delete button for the row; for instance, Apple’s Mail application that ships with iOS takes this approach. Just adding the following method back into CGViewController.m will reimplement this functionality:
- (void) tableView:(UITableView *)tv commitEditingStyle:(UITableViewCellEditingStyle) editing forRowAtIndexPath:(NSIndexPath *)indexPath { if( editing == UITableViewCellEditingStyleDelete ) { [cities removeObjectAtIndex:indexPath.row]; [tv deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationLeft]; } }
As promised in Chapter 5, I’m going to talk about the image picker view controller. This view controller manages Apple-supplied interfaces for choosing images and movies, and on supported devices, it takes new images or movies with the camera. As this class handles all of the required interaction with the user, it is very simple to use. All you need to do is tell it to start, and then dismiss it after the user selects an image or movie.
In this section, we’ll continue to build on our City Guide application. Either of the two versions of the application we now have will do, as all of the changes we’re going to make will be confined to the AddCityController
class. In the preceding section, we made only relatively minor changes in this class that won’t affect our additions here.
However, if you want to follow along, I’m going to return to our original version and work on that. As we did in the preceding section, you should work on a copy of the project, so right-click or Control-click on the folder containing the project files and select Duplicate. A folder called CityGuide copy will be created containing a duplicate of our project. You should probably rename the folder to something more sensible. I suggest CityGuide3, and then after opening it in Xcode, rename the project as we did in the last section by clicking on the blue Project icon at the top of the Project navigator and hitting the Enter key (see Figure 6-26).
The first thing we need to do is build an interface to allow the user to trigger the image picker. If you remember from Chapter 5, our Add City view was built out of two custom table view cells. The easiest way to add this ability is to add another table view cell.
Click on the AddCityController.xib file to open it in Interface Builder. Drag and drop a table view cell (UITableViewCell
) from the Object Library into the Editor window; set the cell selection type from Blue to None in the Attributes Inspector.
We need to resize this cell so that it can hold a small thumbnail of our selected image, so go to the Size Inspector and change its height from the default 44 points to H = 83 points.
Go back to the new cell and grab a label (UILabel
) from the Object Library and drop it onto the tableview cell. In the Attributes Inspector, change the label’s text to “Add a picture:” and then position the label left-and-centered inside the cell.
Next, drop a round rect button (UIButton
) onto the cell, and in the Attributes Inspector, change its type from Rounded Rect to Add Contact. The button should now appear as a blue circle enclosing a plus sign. Position it right-and-centered in the cell.
Finally, grab an image view (UIImageView
) from the Object Library and drop it onto the cell, and resize it to be W = 83 and H = 63 using the Size Inspector. Then in the Attributes Inspector, set the Tag attribute to 777 (as this lets us easily refer to this subview from our code) and set the view mode to Aspect Fill.
After doing this, you should have something that looks a lot like Figure 6-27. Make sure you’ve saved your changes to the nib file, and then open the AddCityController.h and AddCityController.m files in Xcode.
In the AddCityController.h interface file, the first thing we need to do is add an IBOutlet
to allow us to connect our code to the new table view cell inside Interface Builder. We must also add an instance variable of type UIImage
called cityPicture
, which we’ll use to hold the image passed back to us from the image picker, along with an addPicture:
method that we’ll connect to the UIButton
in the cell, allowing us to start the image picker. Add the lines shown in bold to the file:
#import <UIKit/UIKit.h> @interface AddCityController : UIViewController <UITableViewDataSource, UITableViewDelegate> { IBOutlet UITableView *tableView; IBOutlet UITableViewCell *nameCell; IBOutlet UITableViewCell *pictureCell; IBOutlet UITableViewCell *descriptionCell; UIImage *cityPicture; } - (IBAction)addPicture:(id)sender; @end
Before implementing the code to go with this interface, we need to quickly go back into Interface Builder and make those two connections. Reopen the AddCityController.xib file and right-click and drag from the blue button in the new add picture table view cell to File’s Owner to connect the button the addPicture:
received action; see Figure 6-28.
Figure 6-28. Connecting the addCity: received action to the UIButton in our new UITableViewCell to allow it to trigger the image picker
Then click and drag from File’s Owner to the table view cell in the editor, as shown in Figure 6-29, and connect the pictureCell
outlet to our newly created table view cell.
We now need to save this file, and then go back into Xcode to finish our implementation. In the AddCityController.m implementation file, first we have to provide a default image for the UIImage
in the cell (otherwise, it will appear blank). We can do this inside the viewDidLoad:
method by adding this line [you’ll need an image called QuestionMark.jpg for this to work; see Capturing the City Data in Chapter 5 for information on using this image in your project]:
cityPicture = [UIImage imageNamed:@"QuestionMark.jpg"];
We also have to make some changes to the table view delegate and data source methods (in the AddCityController.m implementation file) to take account of the new cell. First we need to change the number of rows returned by the tableView:numberOfRowsInSection:
method from two to three. Make the change shown in bold:
- (NSInteger)tableView:(UITableView *)tv
numberOfRowsInSection:(NSInteger)section {
return 3;
}
Now we need to modify the tableView:cellForRowAtIndexPath:
method to return the extra cell in the correct position in our table view. Make the changes shown in bold:
- (UITableViewCell *)tableView:(UITableView *)tv cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = nil; if( indexPath.row == 0 ) { cell = nameCell; } else if ( indexPath.row == 1 ) { UIImageView *pictureView = (UIImageView *)[pictureCell viewWithTag:777]; pictureView.image = cityPicture; cell = pictureCell; } else { cell = descriptionCell; } return cell; }
In the first row of the table view, we return a nameCell
, configured to allow the user to
enter the city name.
In the second row of the table view, we return the cell we
just added. We first populate the UIImageView
with the image held by the
cityPicture
variable that we
initialized in the viewDidLoad:
method earlier.
Finally, we return the table view cell that we set up to allow the user to enter a description for the city.
We also need to change the tableView:heightForRowAtIndexPath:
method to take account of the new cell. Make the changes shown in bold:
- (CGFloat)tableView:(UITableView *)tv heightForRowAtIndexPath:(NSIndexPath *)indexPath { CGFloat height; if( indexPath.row == 0 ) { height = 44; } else if( indexPath.row == 1 ) { height = 83; } else { height = 440; } return height; }
Finally, we need to add a placeholder implementation for our addPicture:
method, which we’ll fill in later:
- (IBAction)addPicture:(id)sender { NSLog(@"addPicture: called."); }
We’re done, at least for now. Click the Run button in the Xcode toolbar to compile and run the application in iPhone Simulator. Once the application has started, tap the Edit button in the navigation bar and click Add New City (if you chose to modify the second version of the guide, click the Add button). Figure 6-30 shows the modified view.
Now we have an interface to trigger the image picker for us, so let’s implement the code to do that. First we need to add a UIImagePickerController
variable to the AddCityController.h interface file. We also need to declare the class to be a delegate. Make the changes shown in bold:
@interface AddCityController : UIViewController <UITableViewDataSource, UITableViewDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate> { IBOutlet UITableView *tableView; IBOutlet UITableViewCell *nameCell; IBOutlet UITableViewCell *pictureCell; IBOutlet UITableViewCell *descriptionCell; UIImage *cityPicture; UIImagePickerController *pickerController; } - (IBAction)addPicture:(id)sender; @end
In the AddCityController.m implementation file, we need to modify the viewDidLoad:
method to initialize our UIImagePickerController
. Make the changes shown in bold:
- (void)viewDidLoad { self.title = @"New City"; self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemSave target:self action:@selector(saveCity:)]; cityPicture = [UIImage imageNamed:@"QuestionMark.jpg"]; pickerController = [[UIImagePickerController alloc] init]; pickerController.allowsEditing = NO; pickerController.delegate = self; pickerController.sourceType = UIImagePickerControllerSourceTypeSavedPhotosAlbum; }
We allocate and initialize the UIImagePickerController
(this means we’re responsible for it and we must release it inside our dealloc:
method).
When using the image picker, the user may be allowed to edit the selected image before it is passed to our code. This disables that option here.
We set the delegate class to be this class.
Finally, we select the image source. There are three: UIImagePickerControllerSourceTypeCamera
, UIImagePickerControllerSourceTypePhotoLibrary
, and UIImagePickerControllerSourceTypeSavedPhotosAlbum
. Each presents different views to the user, allowing her to take an image with the camera, pick it from the image library, or choose something from her photo album.
We also need to implement the addPicture:
method, the method called when we tap the button in our interface. This method simply starts the image picker interface, presenting it as a modal view controller. Replace the placeholder addPicture:
method you added to the AddCityController.m file as part of the instance methods pragma section with the following:
- (IBAction)addPicture:(id)sender { [self presentViewController:pickerController animated:YES completion:nil]; }
Next, we need to implement the delegate method that will tell our code the user has finished with the picker interface—the imagePickerController:didFinishPickingMediaWithInfo:
method. Add the following to AddCityController.m:
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info { [[self dismissViewControllerAnimated:YES completion:nil]; cityPicture = [info objectForKey:@"UIImagePickerControllerOriginalImage"]; UIImageView *pictureView = (UIImageView *)[pictureCell viewWithTag:777]; pictureView.image = cityPicture; [tableView reloadData]; }
We dismiss the image picker interface; we don’t need to add a completion block at this time so we just pass nil.
We grab the UIImage
selected by the user from the NSDictionary
returned by the image picker
and set the cityPicture
variable.
We grab a reference to the thumbnail UIImageView
, populate it with the chosen
image, and reload the table view so that the displayed image is
updated.
Finally, in the saveCity:
method, we need to add a line just before we add the new
City
to the cities
array. Add the line shown in bold:
newCity.cityPicture = nil;
newCity.cityPicture = cityPicture;
[cities addObject:newCity];
This will take our new picture and serialize it into the data model for our application.
It’s time to test our application. Make sure you’ve saved your changes and click on the Run button.
If you test the application in iPhone Simulator, you’ll notice that there are no images in the Saved Photos folder. There is a way around this problem. In the simulator, tap the Safari icon and drag and drop a picture from your computer (you can drag it from the Finder or iPhoto) into the browser. You’ll notice that the URL bar displays the file path to the image. Click and hold down the cursor over the image and a dialog will appear allowing you to save the image to the Saved Photos folder.
Once the application has started, tap the Edit button in the navigation bar and go to the New City view. Tapping the blue button will open the image picker, as shown in Figure 6-31, and allow you to select an image. Once you’ve done that, the image picker will be dismissed, and you’ll return to the New City interface.
Is everything working? Not exactly; depending on how you tested the interface you may have noticed the problem. Currently, if you enter text in the City field and then click on the “Add a picture” button before clicking on the Description field, the text in the City field will be lost when you return from the image picker. However, if you enter text in the City field and then enter text in (or just click on) the Description field, the text will still be there when you return from the image picker. Any text entered in the Description field will remain in any case.
This is actually quite a subtle bug and is a result of the different ways in which a UITextField
and UITextView
interact as first responders. We’re going to talk about the responder chain in Chapter 8 when we deal with data handling in more detail. However, to explain this without getting into too much detail, the first responder is the object in the application that is the current recipient of any UI events (such as a touch). The UIWindow
class sends events to the registered first responder, giving it the first chance to handle the event. If it fails to do so, the event will be passed to the next object.
By default, the UITextField
doesn’t commit any changes to its text until it is no longer the first responder, which is where the problem comes from. While we could change this behavior through the UITextFieldDelegate
protocol, there is a simpler fix. Add the lines shown in bold to the addPicture:
method:
- (IBAction)addPicture:(id)sender { UITextField *nameEntry = (UITextField *)[nameCell viewWithTag:777]; [nameEntry resignFirstResponder]; [self presentModalViewController:pickerController animated:YES]; }
With this change, we force the UITextField
to resign as first responder before we open the image picker. This means that when the image picker is dismissed, the text we entered before opening it will remain when we are done.
Save your changes, and then click on the Run button in the Xcode toolbar. When the application starts up, return to the New City view and confirm that this simple change fixes the bug.
We’re done with the City Guide application for a while. However, we’ll be back in Chapter 8, where I’ll fix the last remaining problem with the application and talk about data storage. Until then, cities you add will not be saved when you exit the application, so don’t enter all your favorite cities just yet.
Up until now we’ve been building examples for the iPhone. There is no particular reason for that—the code we’ve looked at so far is going to function identically on the iPad, and in fact, a good exercise at this point would be to go back and rebuild one of our previous applications for the iPad. You’ll find it easy going.
I’ve been using applications targeted to the iPhone first because of exactly that point—that the code would be identical between the two platforms—but mostly because more people have an iPhone, or an iPod touch, than have an iPad.
However, sometimes the code isn’t the same, and this is the case of the Xcode Master-Detail Application template. On the iPhone it is implemented using a UINavigationController
; however, on the iPad, it is implemented using a UISplitViewController
. This is a container view controller that manages two panes of information, and is iPad only; there is no equivalent controller for the iPhone.
Open up Xcode and create a new project and choose the Master-Detail template from the drop-down, as shown in Figure 6-32.
When prompted, name the project MDExample
and ensure that the ARC checkbox is ticked; however, for the first time, set the Device Family to Universal rather than iPhone (or iPad for that matter), as shown in Figure 6-33.
This will create a universal application, which will run on both the iPhone and the iPad. You may have come across these in iTunes; they’re marked with the small plus symbol on the application’s icon.
A universal application will generally have separate nib files for iPhone and iPad (see Figure 6-34) along with application logic to handle that. Essentially, if we think about things using the MVC pattern, the views (nib files) and controllers (view controllers) are separate, while the model (the backend representation of our data) is shared.
Let’s look at the application delegate. Click on the AppDelegate.m file to open it in the Standard Editor. If you look at the application:didFinishLaunchingWithOptions:
method, you’ll see that it the application launches and does very different things depending on whether it is running on an iPhone or an iPad.
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) { // The application is running on an iPhone or iPod touch } else { // The application is running on an iPad } [self.window makeKeyAndVisible]; return YES; }
It does this by checking the userInterfaceIdiom
in the shared UIDevice
class:
[[UIDevice currentDevice] userInterfaceIdiom]
Currently, this can either be UIUserInterfaceIdiomPhone
or UIUserInterfaceIdiomPad
. If the application detects that it is running on an iPhone, it runs the following code block:
MasterViewController *masterViewController = [[MasterViewController alloc] initWithNibName: @"MasterViewController_iPhone" bundle:nil]; self.navigationController = [[UINavigationController alloc] initWithRootViewController: masterViewController]; self.window.rootViewController = self.navigationController;
The MasterViewController
is a
UITableViewController
; up until now we’ve built
our table views by hand using UIViewController
s.
However, a UITableViewController
is an
Apple-provided convenience class that handles some of the heavy
lifting involved in managing a table view. It provides default
connections to the table view’s data source and delegate and does
some behind-the-scenes management. However, it’s not magic, it’s
just a subclass of UIViewController
.
We set the root view of our
UINavigationController
to be our table
view.
We set the root view controller of our applications window to be the navigation controller.
Apart from the appearance of the UITableViewController
, the Apple-provided convenience class for handling table views, this is all pretty standard stuff and shouldn’t be new to you. However, something very different happens if the application is running on an iPad. Here, three distinct things happen. First, the master view controller is created and assigned as the root view of a navigation controller:
MasterViewController *masterViewController = [[MasterViewController alloc] initWithNibName:@"MasterViewController_iPad" bundle:nil]; UINavigationController *masterNavigationController = [[UINavigationController alloc] initWithRootViewController: masterViewController];
Then we create a separate detail view alongside the master view, and assign it as the root view of a separate navigation controller:
DetailViewController *detailViewController = [[DetailViewController alloc] initWithNibName:@"DetailViewController_iPad" bundle:nil]; UINavigationController *detailNavigationController = [[UINavigationController alloc] initWithRootViewController:detailViewController];
Finally, a UISplitViewController
is created, the detail view controller is set as the delegate class, and the two managing navigation controllers are passed as an array to the split view controller:
self.splitViewController = [[UISplitViewController alloc] init]; self.splitViewController.delegate = detailViewController; self.splitViewController.viewControllers = [NSArray arrayWithObjects:masterNavigationController, detailNavigationController, nil]; self.window.rootViewController = self.splitViewController;
If you’re finding the code in the universal application a bit overwhelming, you might want to create two separate applications based on the Master-Detail Application template; one targeted for the iPhone, the other for the iPad. Then open both projects in Xcode and compare the now split code base.
You can take a look at the difference this makes by running the template application in the iPhone and iPad Simulators and seeing how the application behaves in both cases. To do so, you should change the Scheme in the drop-down menu in the Xcode toolbar (see Figure 6-35), in exactly the same way you choose to target the simulator or the device for a normal build, and then click the Run button to build and deploy your application into the relevant simulator (or onto your device for that matter).
Running the application on both simulators should hopefully make clear the difference between the two user interfaces (see Figure 6-36).
The iPhone interface is a standard table view interface, much like the interfaces we’re talked about up until now, while the iPad interface is different. You’ll have come across this view controller in the Mail application that ships with the iPad.
You might notice that, at least by default, in portrait orientation the split view controller shows only the larger (detail) panel and provides a toolbar button for displaying the first panel (the table view) using another view that slides in from the left. However, if you rotate the iPad or the iPad Simulator using the Hardware→Rotate Right (or Rotate Left) menu item, you’ll see that this view changes and the master view is inset to the left of the detail view.
Figure 6-36. The Master-Detail Application in the iPad (left) using a UISplitViewController and the iPhone (right) using a standard UINavigationController
If this is not the behavior you want—for instance, the Settings application that ships with the iPad displays the master controller to the left of the detail controller in both landscape and portrait orientations—you can change it. In the application delegate, we set the detail view controller as the UISplitViewControllerDelegate
and we can use this delegate protocol to tell the split view controller how to display the two controllers.
Add the following code to the DetailViewController.m implementation:
- (BOOL)splitViewController: (UISplitViewController*)svc shouldHideViewController:(UIViewController *)vc inOrientation:(UIInterfaceOrientation)orientation { return NO; }
and then click the Run button to rebuild the application and run it in the iPad Simulator. You should see the interface has now changed to place the two controllers next to each other.
Because the two panes (master and detail) are destined to contain application-specific content, there isn’t any default interaction between the two controllers; it’s up to us to provide and manage those in code.
If you take a look at the DetailViewController.[h,m] interface and implementation files, you should notice that the class provides two properties: a detailItem
and a detailDescriptionLabel
. The setDetailItem:
method provided by the template, which overrides the default method generated by synthesizing the detailItem
property, calls a further configureView
method, which in turn takes the description
of the object passed to the controller and provides that as the text in the associated UILabel
in the DetailViewController
nib file.
If you tap the “+” button a couple of times to add a few items to the table view in the master controller, and then go ahead and tap on one of these in turn, you’ll see that the description in the detail view pane changes to be the same as the item you selected.
I mentioned earlier in the chapter that we were going to take a more detailed look at popover controllers. Although not actually a view controller itself, the class manages the presentation of view controllers.
Before the arrival of iOS 6 the UISplitViewController
used a Popover Controller to present the master view when the iPad was in portrait mode rather than sliding a separate view in from the left. Like the UISplitViewController
we looked at in the last section, the popover controller is only available on the iPad. There is no equivalent on the iPhone.
You can associate a popover controller with any piece of your UI—for instance, allowing you to present detailed information about a specific piece of your user interface, or a submenu of controls. We can create a UIPopoverController
very simply, for instance in a button callback:
- (IBAction)itemTapped:(id)sender { UIViewController* content = [[UIViewController alloc] init]; UIPopoverController* popover = [[UIPopoverController alloc] initWithContentViewController:content]; popover.delegate = self; self.popoverController = popover; [self.popoverController presentPopoverFromBarButtonItem:sender permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES]; }
Popovers are dismissed automatically when the user taps outside the popover view. Taps within the popover do not cause it to be automatically dismissed. When a popover is dismissed due to user taps outside the popover view, the popover automatically notifies its delegate of the action.
It’s not necessary to tie the presentation of the popup to a button; you can also attach the popover to a rectangular area in your main view. Depending on where it is positioned, it may appear at any side of the “hot spot.” The code for attaching it this way is almost identical to the first:
[self.popoverController presentPopoverFromRect:CGRectMake(10.0f, 10.0f, 10.0f, 10.0f) inView:self.view permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
Here, instead of attaching it to a button, we are attaching it to an arbitrary rectangle inside our view constructed using the CGRectMake
method call (where the parameters of CGRectMake
are x
, y
, width
, and height
). You can get the CGRect
of an arbitrary UIView
, such as a button or other user interface element, by calling:
CGRect frame = view.frame;