Earlier in this book, you learned about UITabBarController
and how it allows a user to access different screens. A tab bar controller is great when you have screens that don’t rely on each other, but what if you want to move between related screens?
For example, the iPhone Settings application has multiple related screens of information: a list of settings (like Sounds), a detailed page for each setting, and a selection page for each detail. This type of interface is called a drill-down interface. In this chapter, you will use a UINavigationController
to add a drill-down interface to Homepwner (Figure 12.1).
When you have an application that presents multiple screens of information, UINavigationController
maintains a stack of those screens. The stack is an NSArray
of view controllers, and each screen is the view
instance controlled by a UIViewController
. When a UIViewController
is on top of the stack, its view
is visible.
When you initialize an instance of UINavigationController
, you give it one UIViewController
. This UIViewController
is called the root view controller, and its position in the stack is shown in Figure 12.2. In the Homepwner application, the root view controller will be ItemsViewController
. It is the first screen the user sees and can navigate from.
The root view controller is always on the bottom of the stack (which is also the top if there is only one item). More UIViewController
s can be pushed on top of this stack during execution. When this happens, the view
of the pushed UIViewController
slides onto the screen. When the stack is popped, the top view controller is removed from the stack, and the view of the one below it slides onto the screen. This ability to add to the stack during execution is missing in UITabBarController
, which must have all of the view controllers it maintains at initialization time. Navigation controllers are more dynamic, and only the root view controller is guaranteed to always be in the stack.
The UIViewController
that is on top of the stack can be accessed by sending the message topViewController
to the UINavigationController
instance. You can also get the entire stack as an NSArray
by sending the navigation controller the message viewControllers
. The viewControllers
array is ordered so that the root view controller is the first entry and the top view controller is the last entry.
UINavigationController
is actually a subclass of UIViewController
, so it also has a view
instance. Its view
always has at least two subviews: a UINavigationBar
and the view
of the UIViewController
that is on top of the stack (Figure 12.3). The only requirements for using a UINavigationController
are that you add its view
to the visible view hierarchy and give it a root view controller.
In this chapter, you will be adding a UINavigationController
to the Homepwner application. When a user selects one of the possession rows, a new UIViewController
’s view will slide onto the screen. That view controller will allow the user to view and edit the properties of the Possession
. The object diagram for the updated Homepwner application is shown in Figure 12.4.
This application is starting to get fairly large, as demonstrated by the massive object diagram. Fortunately, view controllers and UINavigationController
know how to deal with this type of complicated object diagram. When writing iPhone applications, it is important to treat each UIViewController
as its own little world. The stuff that has already been implemented in Cocoa Touch will do the heavy lifting.
In Homepwner.xcodeproj
that you created earlier, open the file HomepwnerAppDelegate.m
. The UINavigationController
instance will now be the window’s rootViewController
(whereas, previously, the root view controller of the window was ItemsViewController
). This UINavigationController
will be initialized with ItemsViewController
as its root view controller. Make these changes in application:didFinishLaunchingWithOptions:
.
Build and run the application. Homepwner will look the same as it did before — except now it has a UINavigationBar
at the top of the screen (Figure 12.5). Notice how ItemsViewController
’s view
was resized to fit the screen with a navigation bar. UINavigationController
did this for you.
The UINavigationBar
isn’t very interesting right now. At a minimum, a UINavigationBar
should display a descriptive title for the UIViewController
that is currently on top of the UINavigationController
’s stack.
Every UIViewController
has a property navigationItem
of type UINavigationItem
. While UINavigationBar
is a subclass of UIView
(which means it can be appear on screen), UINavigationItem
is not. However, it supplies the navigation bar with the content it needs to draw. When a UIViewController
comes to the top of a UINavigationController
’s stack, the navigation controller’s UINavigationBar
uses the UIViewController
’s navigationItem
to configure itself, as shown in Figure 12.6.
That’s not the easiest thing to understand at first glance. So, consider the following analogy. Think of UIViewController
as an NFL football team, and moving to the top of the stack as going to the Super Bowl. The UINavigationItem
is the team logo design, and, no matter what, its team logo remains unchanged; it’s an internal design. The UINavigationController
is the stadium, and the UINavigationBar
is an end zone. When a team makes it to the Super Bowl, its team logo is painted on one end zone of the stadium. And when a UIViewController
is moved to the top of the stack, its UINavigationItem
is painted on the UINavigationBar
within the UINavigationController
.
By default, a UINavigationItem
is empty. At the most basic level, a UINavigationItem
has a simple title
string. When a UIViewController
is moved to the top of the navigation stack and its navigationItem
has a valid string for its title
property, the navigation bar will display that string (Figure 12.7).
A navigation item can hold more than just a title string, as shown in Figure 12.8. There are three customizable areas for each UINavigationItem
: a titleView
, a leftBarButtonItem
, and a rightBarButtonItem
. The left and right bar button items are pointers to instances of UIBarButtonItem
, a type of button that can only be displayed on a UINavigationBar
or a UIToolbar
.
Like UINavigationItem
, UIBarButtonItem
is not a subclass of UIView
but supplies the content that a UINavigationBar
needs to draw. Consider the UINavigationItem
and its UIBarButtonItem
s to be containers for strings, images, and other content. A UINavigationBar
knows how to look in those containers and draw the content that’s there.
The third customizable area of a UINavigationItem
is its titleView
. You have a choice with each navigation item: use a basic string as the title (as you’ll do in this chapter) or have any subclass of UIView
sit in the center of the navigation item. You cannot have both. If it suits the context of a specific view controller to have a custom view (such as a button, a slider, an image view, or even a map) instead of a title, you would set the titleView
of the navigation item to that custom view. Typically, however, a title string is sufficient.
Set up ItemsViewController
to have a proper navigationItem
. Update the init
method by adding the following lines of code to ItemsViewController.m
.
Building and running the application now will show a lovely UINavigationBar
with a title and — surprise! — an Edit button. Go ahead and tap that Edit button and watch the UITableView
enter editing mode! Where did editButtonItem
come from? Every UIViewController
has a editButtonItem
property. When sent editButtonItem
, the view controller creates a UIBarButtonItem
with the title Edit. This button came with a target-action pair: it will send the message setEditing:animated:
to its UIViewController
when tapped.
This means you can simplify the code a bit. You no longer need the header view with the button labeled Edit. To get rid of the header view, delete the following two methods from ItemsViewController.m
.
The headerView
will no longer be used, and your code will still build the correct application. Also, you will want to remove the instance variable headerView
along with the implementation of the methods headerView
and editingButtonPressed:
.
Now you can build and run again. The old Edit button is gone, and you have a much more efficient editButtonItem
in the UINavigationBar
that does the same thing (Figure 12.9).
To see the real power of UINavigationController
, you need another UIViewController
to put on its stack. Create a new UIViewController
subclass by selecting New File... from the File menu. Choose UIViewController subclass and select With XIB for user interface only. Name this class ItemDetailViewController
and add it to the Homepwner
project (Figure 12.10).
In Homepwner, the user will be able to tap one of the rows and have another view slide onto the screen with editable text fields for each property of that Possession
. This view will be controlled by an instance of ItemDetailViewController
.
You need four subviews – one for each instance variable of a Possession
instance. ItemDetailViewController
’s view
will display these and allow the user to edit them. And because you need to be able to access these subviews during runtime, ItemDetailViewController
needs outlets for these subviews. Add the following instance variables to ItemDetailViewController.h
.
Save this file. The IBOutlet
in front of each of these instance variables should clue you into the fact you are going to use Interface Builder to lay out the interface for ItemDetailViewController
’s view
. When you created ItemDetailViewController
, a XIB file of the same name was created and added to the project. Open ItemDetailViewController.xib
now.
You have seen XIB file in previous exercises. You’ve also added subviews to the window, made outlet connections, and connected action messages. In those XIB files, there was a File’s Owner object in the doc Window that you used without really understanding. Now it is time to learn what the File’s Owner really is.
File’s Owner is a placeholder for an object that is supplied when the NIB file is read in. That is, File’s Owner is a hole, and whatever causes the NIB file to be unarchived, supplies something to go into that hole.
This is a little abstract because you have never explicitly unarchived a NIB file. Instead, the UIApplication
object implicitly unarchived the MainWindow.nib
file, and your view controllers have implicitly unarchived their NIB files. How does this work? When a view controller loads its NIB file, it will supply itself to fill the role of File’s Owner. The implementation of loadView
in UIViewController
looks something like this:
So, this object (which exists before the NIB file is read in) gets wired to the newly created objects.
Back in ItemDetailViewController.xib
, double-click the View
object in the doc window. This view will be the view
of ItemDetailViewController
when it is loaded from this XIB file. (Don’t believe me? Check the connections for the File’s Owner.) Drag four UILabel
s and three UITextField
s from the Library window onto the view so that it matches Figure 12.11.
Make connections from the File’s Owner to each of these objects, as shown in Figure 12.11.
For each UITextField
instance, uncheck the Clear When Editing Begins checkbox on the Inspector window (Figure 12.12). Save this XIB file and quit Interface Builder.
While you are here, fancy the application up a bit. Right now, the view
for ItemDetailViewController
has a plain white background. Let’s give it the same background as the UITableView
. When should you do this? After a UIViewController
loads its view
, it is immediately sent the message viewDidLoad
. Whether that view is loaded from a XIB file or using the method loadView
, this message gets sent to the view controller. If you need to do any extra initialization to a UIViewController
that requires its view
to already exist, you must override viewDidLoad.
(Remember, instantiating a view controller doesn’t create the view. The view is created only when it is needed.) Override viewDidLoad
in ItemDetailViewController.m
.
When ItemDetailViewController
’s view
gets unloaded, its subviews will still be retained by ItemDetailViewController
. They need to be released and set to nil
in viewDidUnload
. Override this method in ItemDetailViewController.m
.
And, finally, you need a dealloc
method:
Now you have a navigation controller, a navigation bar, and two view controllers. Time to put all the pieces together. The user should be able to tap one of the rows in ItemsViewController
’s table view and have the ItemDetailViewController
’s view slide onto the screen and display the properties of the selected Possession
instance.
Of course, you then need to create an instance of ItemDetailViewController
. Where should this object be created and what object should hold the pointer to it? Think back to previous exercises where you instantiated all of your controllers in the method application:didFinishLaunchingWithOptions:
. For example, in the tab bar controller chapter, you created both view controllers and immediately added them to tab bar controller’s viewControllers
array.
However, when using a UINavigationController
, you cannot simply store all of the possible view controllers in its stack. The viewControllers
array of a navigation controller is dynamic – you start with a root view controller, and additional view controllers are added depending on user input. Therefore, some object other than the navigation controller needs to own the instance of ItemDetailViewController
and be responsible for adding it to the stack. This owner needs two things: it needs to know when to push ItemDetailViewController
onto the stack, and it needs a pointer to the navigation controller. Why must this object have a pointer to the navigation controller? If it is to dynamically add view controllers to the navigation controller’s stack, it must be able to send the navigation controller messages, namely, pushViewController:animated:
.
ItemsViewController
meets both of these needs. Whenever a row is tapped in a table view, the table view’s delegate receives the message tableView:didSelectRowAtIndexPath:
. Therefore, ItemsViewController
knows when to push the other view controller on the stack. Furthermore, when a view controller belongs to a navigation controller’s stack, it can be sent the message navigationController
to get a pointer to the navigation controller it belongs to. As the root view controller, ItemsViewController
always belongs to the navigation controller and thus can always access it.
In any application that uses a UINavigationController
, there is one root view controller. It often owns the next view controller, and the next view controller owns the one after that and so on. Some applications, like the Photos application, may have more than one combination of view controllers that can be on the stack at a given time. In Photos, there are four view controllers:
Therefore, AlbumListViewController
owns AlbumViewController
. AlbumViewController
owns both ImageViewController
and VideoViewController
(Figure 12.13).
Back in ItemsViewController.h
, add an instance variable for an ItemDetailViewController
.
Recall that when a row is tapped, its delegate is sent a message containing the index path of the selected row. In ItemsViewController.m
, implement this method to lazily allocate the ItemDetailViewController
and then push it on top of the navigation controller’s stack.
Finally, at the top of ItemsViewController.m
, import the header file for ItemDetailViewController
.
Build and run the application. Select one of the rows from the UITableView
. Not only will you be taken to ItemDetailViewController
’s view
, but you will get a free animation and a button in the UINavigationBar
titled Homepwner. Tapping this button will take you back to ItemsViewController
. All of that comes for free. Thanks, UINavigationController
!
Of course, the UITextField
s on the screen are currently empty. How do you pass data between these two UIViewController
s? You have all of the Possession
s in ItemsViewController
, and you want to display a single Possession
in ItemDetailViewController
. You need to implement a method in ItemDetailViewController
that will take a Possession
instance and fill the contents of its UITextField
s with it. ItemsViewController
will select the appropriate possession from its array and pass it through that method to the ItemDetailViewController
.
In ItemDetailViewController.h
, add an instance variable to hold the Possession
that is being edited and declare a method to set that instance variable. The class declaration should now look like this:
Use @synthesize
to create accessors for editingPossession
in ItemDetailViewController.m
.
@implementation ItemDetailViewController
@synthesize editingPossession;
At the top of ItemDetailViewController.m
, make sure to import the header file for the Possession
class.
When the ItemDetailViewController
’s view
appears on the screen, it needs to set the values of its subviews to match the properties of the editingPossession
. Override viewWillAppear:
in ItemDetailViewController.m
to transfer the editingPossession
’s properties to the various UITextField
s.
Now you must invoke this method when the ItemDetailViewController
is being pushed onto the navigation stack. Add the following line of code to this method in ItemsViewController.m
.
Many programmers new to the iPhone SDK struggle with how data is passed between UIViewController
s. The technique you just implemented, having all of the data in the root view controller and passing subsets of that data to the next UIViewController
, is a very clean and efficient way of performing this task.
Build and run your application. Select one of the rows of the UITableView
, and the view that appears on your screen will contain all of the information for the Possession
that was in that row. While you can edit this data, the UITableView
won’t have changed when you return to it. To fix this problem, you need to implement code to update the properties of the Possession
being edited.
Whenever a UINavigationController
is about to swap views, it sends out two messages: viewWillDisappear:
and viewWillAppear:
. The UIViewController
that is about to be popped off the stack is sent the message viewWillDisappear:
. The UIViewController
that will then be on top of the stack is sent viewWillAppear:
.
When ItemDetailViewController
is popped off the stack, you will set the properties of the editingPossession
to the values in the UITextField
s. When implementing these methods for views appearing and disappearing, it is important to call the superclass’s implementation – it has some work to do as well. Implement viewWillDisappear:
in ItemDetailViewController.m
.
Now the values of the Possession
will be updated when the user taps the Homepwner back button on the UINavigationBar
. When ItemsViewController
appears back on the screen, it is sent the message viewWillAppear:
. Take this opportunity to reload its UITableView
so the user can immediately see the changes. Implement that viewWillAppear:
in ItemsViewController.m
.
Build and run your application now. You will be able to move back and forth between each of the UIViewController
s you created and change the data with ease.
The keyboard for the UITextField
that displays a Possession
’s valueInDollars
is a QWERTY keyboard. It would be better if it was a number pad. Change the Keyboard Type of that UITextField
to the Number Pad. (Hint: You can do this in Interface Builder in the Attributes tab of the Inspector.)