12. UINavigationController

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).

Figure 12.1. Homepwner with UINavigationController

image

UINavigationController

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.

Figure 12.2. UINavigationController’s stack

image

The root view controller is always on the bottom of the stack (which is also the top if there is only one item). More UIViewControllers 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.

Figure 12.3. A UINavigationController’s view

image

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.

Figure 12.4. Homepwner object diagram

image

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:.

image

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.

Figure 12.5. Homepwner with an empty navigation bar

image

UINavigationBar

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.

Figure 12.6. UINavigationItem

image

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).

Figure 12.7. UINavigationItem with title

image

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.

Figure 12.8. UINavigationItem with everything

image

Like UINavigationItem, UIBarButtonItem is not a subclass of UIView but supplies the content that a UINavigationBar needs to draw. Consider the UINavigationItem and its UIBarButtonItems 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.

image

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.

image

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).

Figure 12.9. Homepwner with navigation bar

image

An Additional UIViewController

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).

Figure 12.10. Creating ItemDetailViewController

image

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.

image

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.

The XIB file and File’s Owner

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:

image

So, this object (which exists before the NIB file is read in) gets wired to the newly created objects.

Setting up ItemDetailViewController

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 UILabels and three UITextFields from the Library window onto the view so that it matches Figure 12.11.

Figure 12.11. ItemDetailViewController’s configured view

image

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.

Figure 12.12. UITextField attributes

image

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.

image

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.

image

And, finally, you need a dealloc method:

image

Navigating with UINavigationController

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:

image

Therefore, AlbumListViewController owns AlbumViewController. AlbumViewController owns both ImageViewController and VideoViewController (Figure 12.13).

Figure 12.13. Controller hierarchy in Photos

image

Back in ItemsViewController.h, add an instance variable for an ItemDetailViewController.

image

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.

image

Finally, at the top of ItemsViewController.m, import the header file for ItemDetailViewController.

image

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 UITextFields on the screen are currently empty. How do you pass data between these two UIViewControllers? You have all of the Possessions 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 UITextFields 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:

image

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.

image

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 UITextFields.

image

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.

image

Many programmers new to the iPhone SDK struggle with how data is passed between UIViewControllers. 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.

Appearing and disappearing views

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 UITextFields. 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.

image

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.

image

Build and run your application now. You will be able to move back and forth between each of the UIViewControllers you created and change the data with ease.

Challenge: Number Pad

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.)

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset