Before we get into our code, let’s talk about the names we use to refer to the various control- lers that make up our application. At the root of our application is the controller whose view gets added to the window, known as the root controller. In our case, the root controller is the navigation controller that will swap in and out all the other views that make up our hierarchy of views.
Here’s where things get a bit confusing. As it turns out, the UINavigationController class refers to a root view controller. For example, the UINavigationController class features methods called initWithRootViewController: and popToRootViewControllerAnimated:. This reference to a root view controller is really talking about the view controller on the bot- tom of the navigation stack, which is the first view presented to the user when launching the application for the first time. This is different from the “root controller,” which is the naviga- tion controller itself. Confusing, right?
In our case, the navigation controller’s root view controller is the six-row view shown in Figure 9-2. This can be a point of confusion, however, since our application’s root view controller (the UINavigationController) will itself have a root view controller. In order to avoid confusion between the two root controllers, we won’t be using the term
“root view controller” for either. Instead, we’ll refer to the application’s root controller as
navController, and as you just saw in the previous section, we’ll refer to navController’s root view controller as FirstLevelController, because it’s the first level in the visual hier- archy presented to the user.
Take a moment to make sense of all this and file it away in permanent storage. And now, back to our regularly scheduled program.
Let’s start by adding an outlet for our application’s root view controller, navController, in NavAppDelegate.h:
#import <UIKit/UIKit.h>
@interface NavAppDelegate : NSObject <UIApplicationDelegate> { UIWindow *window;
UINavigationController *navController;
}
@property (nonatomic, retain) IBOutlet UIWindow *window;
*navController;
@end
Next, we need to hop over to the implementation file and add the @synthesize statement for navController. We’ll also add navController’s view as a subview of our application’s window so that it gets shown to the user. Single-click NavAppDelegate.m, and make the fol- lowing changes:
#import "NavAppDelegate.h"
@implementation NavAppDelegate
@synthesize window;
@synthesize navController;
- (void)applicationDidFinishLaunching:(UIApplication *)application {
// Override point for customization after application launch [window addSubview: navController.view];
[window makeKeyAndVisible];
}
- (void)dealloc { [window release];
[navController release];
[super dealloc];
}
@end
Save both of these files. Next, we have to create a navigation controller, connect it to the
navController outlet we just declared, and then tell the navigation controller what to use as its root view controller.
Expand the Resources folder in the Groups & Files pane if necessary; then double-click MainWindow.xib to open that file in Interface Builder. Look in the library for a Navigation Controller (see Figure 9-9), and drag one over to the nib’s main window, which is the win- dow labeled MainWindow.xib, not the one labeled Window.
Control-drag from the Nav App Delegate icon to the new Navigation Controller icon, and select the nav- Controller outlet. We’re almost done, but the next task
is a little tricky. We need to tell the navigation controller where to find its root view control- ler. The easiest way to do that is to change the nib’s main window into list mode using the middle View Mode button in the toolbar of that window (see Figure 9-10).
Figure 9-9. The Navigation Controller in the library
Download at Boykma.Com
Click the little disclosure triangle to the left of Navi- gation Controller to expand it. Underneath it, you’ll find two items, Navigation Bar and View Controller (Root View Controller).
Single-click the View Controller (Root View Controller) icon, and press ⌘4 to bring up the identity inspector.
Change the underlying class to FirstLevelViewCon- troller, and press return to commit the change.
Switch to the attributes inspector using ⌘1. Here, if we wanted to, we could also specify a nib file from which it should load the root-level view. Instead, we’re going to leave the NIB Name field blank, which
is how we indicate that the table view controller should create a table view instance for us.
That’s all the changes we need here, so save, close the window, and go back to Xcode.
Now, of course, we need a list for our root view to display. In the last chapter, we used simple arrays of strings. In this application, the first level view controller is going to manage a list of its subcontrollers, which we will be building throughout the chapter. Tapping any row will cause an instance of the selected view controller to get pushed onto the navigation control- ler’s stack. We also want to be able to display an icon next to each row, so instead of adding a UIImage property to every subcontroller that we create, we’re going to create a subclass of UITableViewController that has a UIImage property to hold the row icon. We will then subclass this new class instead of subclassing UITableViewController directly, and as a result, all of our subclasses will get that UIImage property for free, which will make our code much cleaner.
We will never actually create an instance of this new class. It exists solely to let us add a com- mon item to the rest of the controllers we’re going to write. In many languages, we would declare this as an abstract class, but Objective-C doesn’t support abstract classes. We can make classes that aren’t intended to be instantiated, but the compiler won’t prevent us from actually creating them the way it does in many other languages. Objective-C is a much more permissive language than most other popular languages, and this can be a little hard to get used to.
Single-click the Classes folder in Xcode, and press ⌘N to bring up the new file assistant.
Select Cocoa Touch Class from the left pane, select Objective-C class, and select NSObject for Subclass of. Give the new file the name SecondLevelViewController.m. Be sure to create the .h file as well. Once the new files are created, select SecondLevelViewController.h, and make the following changes:
Figure 9-10. Switching MainWindow.
xib’s main window into list mode
#import <UIKit/UIKit.h>
@interface SecondLevelViewController : NSObject {
@interface SecondLevelViewController : UITableViewController { UIImage *rowImage;
}
@property (nonatomic, retain) UIImage *rowImage;
@end
Over in SecondLevelViewController.m, add the following line of code:
#import "SecondLevelViewController.h"
@implementation SecondLevelViewController
@synthesize rowImage;
@end
Any controller class that we want to implement as a second-level controller—in other words, any controller that the user can navigate directly to from the first table shown in our application—should subclass SecondLevelViewController instead of
UITableViewController. Because we’re subclassing SecondLevelViewController, all of those classes will have a property they can use to store a row image, and we can write our code in FirstLevelViewController before we’ve actually written any concrete second- level controller classes by using SecondLevelViewController as a placeholder.
Let’s do that now. First, declare an array in FirstLevelViewController.h:
#import <UIKit/UIKit.h>
@interface FirstLevelViewController : UITableViewController { NSArray *controllers;
}
@property (nonatomic, retain) NSArray *controllers;
@end
The array we just added will hold the instances of the second-level view controllers. We’ll use it to feed data to our table.
Add the following code to FirstLevelViewController.m, and then come on back and gossip with us, ’K?
#import "FirstLevelViewController.h"
#import "SecondLevelViewController.h"
@implementation FirstLevelViewController
@synthesize controllers;
- (void)viewDidLoad {
self.title = @"First Level";
Download at Boykma.Com
NSMutableArray *array = [[NSMutableArray alloc] init];
self.controllers = array;
[array release];
[super viewDidLoad];
}
- (void)viewDidUnload { self.controllers = nil;
[super viewDidUnload];
}
- (void)dealloc {
[controllers release];
[super dealloc];
}
#pragma mark -
#pragma mark Table Data Source Methods
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [self.controllers count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *FirstLevelCell= @"FirstLevelCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:
FirstLevelCell];
if (cell == nil) {
cell = [[[UITableViewCell alloc]
initWithStyle:UITableViewCellStyleDefault reuseIdentifier: FirstLevelCell] autorelease];
}
// Configure the cell
NSUInteger row = [indexPath row];
SecondLevelViewController *controller = [controllers objectAtIndex:row];
cell.textLabel.text = controller.title;
cell.imageView.image = controller.rowImage;
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
return cell;
}
#pragma mark -
#pragma mark Table View Delegate Methods - (void)tableView:(UITableView *)tableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath { NSUInteger row = [indexPath row];
SecondLevelViewController *nextController = [self.controllers objectAtIndex:row];
[self.navigationController pushViewController:nextController animated:YES];
}
@end
The first thing we want you to notice is that we’ve imported that new SecondLevelView- Controller.h header file. Doing that lets us use the SecondLevelViewController class in our code so that the compiler will know about the rowImage property.
Next comes the viewDidLoad method. The first thing we do is set self.title. A naviga- tion controller knows what to display in the title of its navigation bar by asking the currently active controller for its title. Therefore, it’s important to set the title for all controller instances in a navigation-based application, so the user knows where they are at all times.
We then create a mutable array and assign it to the controllers property we declared earlier. Later, when we’re ready to add rows to our table, we will add view controllers to this array, and they will show up in the table automatically. Selecting any row will automatically cause the corresponding controller’s view to get presented to the user.
Tip
Did you notice that our property is declared as an NSArray, but that we’re creating an NSMutable Array? It’s perfectly acceptable to assign a subclass to a property like this. In this case, we use the muta- ble array in viewDidLoad to make it easier to add new controllers in an iterative fashion, but we leave the property declared as an immutable array as a message to other code that they shouldn’t be modifying this array.
The final piece of the viewDidLoad method is the call to [super viewDidLoad]. We do this because we are subclassing UITableViewController. You should always call [super viewDidLoad] when you override the viewDidLoad method, because there’s no way to know if our parent class does something important in its own viewDidLoad method.
The tableView:numberOfRowsInSection: method here is identical to ones you’ve seen in the past; it simply returns the count from our array of controllers. The tableView:cellF orRowAtIndexPath: method is also very similar to ones we’ve written in the past. It gets a dequeued cell, or creates a new one if there aren’t any, and then grabs the controller object from the array corresponding to the row being asked about. It then sets the cell’s textLabel
and image properties using the title and rowImage from that controller.
Notice that we are assuming the object retrieved from the array is an instance of Second LevelViewController and are assigning the controller’s rowImage property to a UIImage. This step will make more sense when we declare and add the first concrete second-level controller to the array in a few minutes.
Download at Boykma.Com
The last method we added is the most important one here, and it’s the only functionality that’s truly new. You’ve seen the tableView:didSelectRowAtIndexPath: method before, of course. It’s the one that gets called after a user taps a row. If tapping a row needs to trig- ger a drill down, this is how we do it. First, we get the row from indexPath:
NSUInteger row = [indexPath row];
Next, we grab the correct controller from our array that corresponds to that row:
SecondLevelViewController *nextController = [self.controllers objectAtIndex:row];
Next, we use our navigationController property, which points to our application’s navi- gation controller, to push the next controller, the one we pulled from our array, onto the navigation controller’s stack:
[self.navigationController pushViewController:nextController animated:YES];
That’s really all there is to it. Each controller in the hierarchy need only know about its children. When a row is selected, the active controller is responsible for getting or creating a new subcontroller, setting its properties if necessary (it’s not necessary here), and then pushing that new subcontroller onto the navigation controller’s stack. Once you’ve done that, everything else is handled automatically by the naviga- tion controller.
At this point, the application skeleton is done. Save all your files, and build and run to make sure all your typing took hold. If all is well, the application should launch, and a navi- gation bar with the title First Level should appear. Since our array is currently empty, no rows will display at this point (see Figure 9-11).
Now, we’re ready to start developing the second-level views.
Before we do that, go grab the image icons from the 09 Nav directory. A subdirectory called Images should have eight .png images, six that will act as row images and an addi- tional two that we’ll use to make a button look nice later in the chapter. Add all eight of them to the Resources folder of your Xcode project before proceeding.
Figure 9-11. The application skeleton in action
Our First Subcontroller: The Disclosure