3.5. Navigation Bars and Controllers

The iPhone doesn't support toolbars in the traditional desktop sense of a clutter of icons across the top of a window. Since each screen of an application is considered a page in a book, Apple has made its version of the toolbar for iPhone to appear more book-like and clean. In contrast to cluttered toolbars, navigation bars include a page title, directional buttons, and text buttons for context-sensitive functions such as turning on a speakerphone or clearing a list of items. Navigation bars also support controls to add tabbed buttons (called segmented controls) such as the "All" and "Missed" call buttons when viewing recent calls. If you miss the cluttered look, you can create toolbars containing custom buttons too. A small library of standard system buttons also exists.

Navigation controllers "wrap" one or more view controllers, allowing you to add a navigation bar to an existing view controller-based application without worrying about handling window dimension changes or orientation. Navigation controls allow you to stack individual view controllers onto it, automatically managing the navigation between them. This provides smoother navigation and handles the work of updating the navigation bar to adjust to each view's navigation properties.

The properties of a navigation bar—the types of buttons and controls to display—are properties of each view controller, not the navigation controller. This allows each view to define its own navigation bar configuration, which the navigation controller loads automatically when a user navigates to the view. If your view controller says, "I have a speakerphone button," the navigation controller will draw a speakerphone button when the view is displayed.

3.5.1. Creating a Navigation Controller

You can create a navigation controller right after you create at least one view controller, and initialize it with a pointer to the top-level view; that is, the authoritative root view for the application. The navigation controller refers to this as the root view controller. The root view controller is the view controller representing the very bottom of your navigation path: the main view for your application that will offer no Back button, and from which other views will originate.

To create a navigation controller, first create the view class that will serve as the root view controller. Then, instantiate the navigation controller using its initWithRootViewController method, as shown below:

viewController = [ [ MyViewController alloc ] init ];

navigationController = [ [ UINavigationController alloc ]
    initWithRootViewController: viewController ];

When using a navigation controller, attach the navigation controller's view to the window. The view controller is anchored to the navigation controller when the navigation controller is created, and so it too will be displayed when the navigation controller is added to the window:

[ window addSubview: [ nagivationController view ] ];

The navigation controller will automatically render both it and the active view controller, which will default to the root view controller. When a new view is pushed onto the navigation controller's stack, the new view is displayed until the user presses the Back button or navigates elsewhere. To push another view onto the navigation controller, use the pushViewController method. An example follows:

[ navigationController pushViewController: myOtherViewController
        animated: YES
];

A Back button will automatically be added to the navigation bar when the view is pushed. The title of the Back button will match the title of the previous view controller on the stack. When the user presses the Back button, the view controller will be peeled off the stack and the previous view underneath will be transitioned back onto the screen.

3.5.2. Navigation Controller Properties

The view controller class shown earlier in this chapter can host its own navigation bar properties. This lets you define a different navigation bar layout for each view controller you create. Up until now, you've only dealt with a single view controller in an application, but in a functional application, you will have one view controller for each navigation screen, each with its own properties determining what the navigation bar should look like.

Navigation bar properties are normally set when the view controller's init method is invoked:

- (void)init {

    self = [ super init ];

    if (self != nil) {

        /* Navigation bar properties */

        self.title = @"My Title";
        ...
    }

    return self;
}

You can change the properties set in the view controller after the navigation controller is displayed—the controller will change its navigation bar to accommodate the view controller's latest settings. All changes to the navigation bar are made through the view controller's properties.

Anything visible in a navigation bar is part of a UINavigationItem object. The UINavigationItem class is the base class for anything that natively attaches to a navigation bar, including buttons and other objects. Apple has made this class private, so you can't access it directly. When each item—such as the title created in the following section—is added to the navigation bar, its UINavigationItem object is pushed onto it like an object on a stack.

3.5.2.1. Setting the title

The navigation title appears as large white text centered in the middle of the navigation bar. The title is frequently used to convey to the end-user what sort of information is being displayed, for example, "Saved Messages." The title also determines the title of the back button, if the view controller is not at the root of the stack:

self.title = @"Inbox";

3.5.2.2. Buttons, styles, and actions

You can add buttons to the left and/or right sides of a navigation bar. Because space is limited, the only buttons that you should add are those for functions specific to navigation or to the page being displayed.

The navigation controller automatically creates a new UINavigationItem object whenever its navigationItem property is accessed. This allows you to assign new objects to the navigation bar without worrying about placement, or even about what the UINavigationItem class looks like.

To create a new navigation button, create an instance of the UIBarButtonItem class. This class allows you to define a button title, style, and action to be invoked when the user presses it:

UIBarButtonItem *myButton = [ [ [ UIBarButtonItem alloc ]
    initWithTitle: @"Do it!"
    style: UIBarButtonItemStylePlain
    target: self
    action: @selector(doit)
] autorelease ];

The following properties are used to initialize the button:



initWithTitle

Sets the title of the button. This argument accepts an NSString, which you've already learned about. If the button is a text button, it will be displayed with this text inside the button.



style

Defines the style of the button. The following styles are available:



UIBarButtonItemStylePlain

Default button style; displays a glow when pressed.



UIBarButtonItemStyleBordered

Same as UIBarButtonItemStylePlain, but displays a bordered button.



UIBarButtonItemStyleDone

Displays a blue button signifying that the user should tap it when he has finished editing.



target

Specifies the delegate for the action. This is the object that will receive notification when the user presses the button. Using self will notify the view controller object, presuming you create the button in the view controller.



action

Specifies the method to invoke when the user presses the button. This notifies the given method name in your target object so it can take the appropriate action.

Once you have created the button, you can add it as the navigation bar's left or right button, at which point it will be displayed immediately:

self.navigationItem.leftBarButtonItem = myButton;
self.navigationItem.rightBarButtonItem = myButton;

To create a button with a left arrow (a Back button), assign the button to the backBarButtonItem property. The Back button will only be displayed if the view controller is not the root view controller:

self.navigationItem.backBarButtonItem = myButton;

When the button is pressed, the selector will be notified on the target object. You'll need to code a method to handle this button press action. The example above specifies the selector doit. Add a method with this same name to your delegate class:

- (void)doit {
    /* Place your action code here */
}

If, at any point, you would like to disable an existing navigation bar button, you have two options. To make the button disappear entirely, use the button properties you read about earlier to set the button to nil:

self.navigationItem.leftBarButtonItem = nil;

To leave the button visible, but grayed out, disable the button by setting its enabled property to NO:

myButton.enabled = NO;

3.5.2.3. Navigation bar style

The navigation controller itself can be displayed in one of a few different styles. The default style is the standard gray appearance. Three different styles are presently supported.

Style Description
UIBarStyleDefault Default style; gray background with white text
UIBarStyleBlackOpaque Solid black background with white text
UIBarStyleBlackTranslucent Transparent black background with white text

The style is set using the barStyle property. This property belongs to the navigation controller, and not the view controller, so that it remains consistent throughout the navigation of all views:

self.navigationController.navigationBar.barStyle = UIBarStyleBlackTranslucent;

                                          

3.5.3. Adding a Segmented Control

Controls are small, self-contained UI components that can be used by various UI Kit classes. They can be glued to many different types of objects, allowing the developer to add additional functionality to a window. One common control found in the navigation bars of Apple's preloaded applications is the segmented control.

You'll notice in many preloaded applications that Apple has added buttons to further categorize the information displayed. For example, the navigation bar in the iTunes WiFi Store application displays "New Releases," "What's Hot," and "Genres" buttons at the top. These further separate the user's music selection choice. Segmented controls are useful for any situation where an overabundance of similar data would best be categorized using two or three buttons.

An example of provisioning a control to display "All" and "Missed" calls follows:

UISegmentedControl *segmentedControl = [ [ UISegmentedControl alloc ]
    initWithItems: nil ];
segmentedControl.segmentedControlStyle = UISegmentedControlStyleBar;
 [ segmentedControl insertSegmentWithTitle: @"All" atIndex: 0 animated: NO ];
 [ segmentedControl insertSegmentWithTitle: @"Missed" atIndex: 1 animated: NO ];

                                          

Once you have created the segmented control, it can be displayed by assigning it to a view controller's titleView navigation property. This causes the standard title text to be replaced with your custom view:

self.navigationItem.titleView = segmentedControl;

You'll also want the class to be notified whenever the user selects a new segment so that it can change to display the new information. To do this, use the UIControl class's addTarget method to assign a method whenever the control value is changed:

[ segmentedControl addTarget: self
    action: @selector(controlPressed:)
    forControlEvents: UIControlEventValueChanged
];

In this example, a selector named controllerPressed was specified as the method that should be notified in the target self. Code this routine into your target class to handle value changes:

- (void) controllerPressed:(id)sender {
    int selectedIndex = [ segmentedControl selectedSegmentIndex ];

    /* Additional code to handle value change */
}

Each button in a segmented control is referred to as a segment. Access the selected segment with a call to the selectedSegment method of the control itself:

- (void) controllerPressed:(id)sender {
    int selectedSegment = segmentedControl.selectedSegmentIndex;
    NSLog(@"Segment %d selected\n", selectedSegment);
}

Chapter 10 explains the UIControl class's event chain and the segmented control in full detail.

3.5.4. Adding a Toolbar

A navigation style (style property) bar can host a number of different types of objects. You've just learned how to attach a segmented control as the navigation bar's title view to present the user with a series of subcategories. Another popular UI component found on the navigation bar is a UIToolbar object. Toolbars can host a custom set of buttons, including standard system buttons such as Bookmarks and Search buttons. Many preloaded iPhone applications, such as Safari and Mail, use toolbars to extend the functionality of the navigation bar.

Before a toolbar can be displayed, you must first create the buttons you intend to display on it. You'll add each button to an array created using Cocoa's NSMutableArray class:

NSMutableArray *buttons = [ [ NSMutableArray alloc ] init ];

3.5.4.1. Image and text buttons

The most common types of buttons are those represented as either images or standard text. Both types of buttons are created as UIBarButtonItem objects, however you will use a different initializer to provision each. Initialize image buttons using the initWithImage method, and initialize standard text buttons using the initWithTitle method:

UIBarButtonItem *buttonImage = [ [ UIBarButtonItem alloc ] initWithImage:
    [ UIImage imageNamed: @"button.png" ]
    style: UIBarButtonItemStylePlain
    target: self
    action: @selector(mySelector:)
];

UIBarButtonItem *buttonText = [ [ UIBarButtonItem alloc ] initWithTitle:
    @"Button"
    style: UIBarButtonItemStyleBordered
    target: self
    action: @selector(mySelector:)
];

3.5.4.2. System buttons

In addition to image and text buttons, a small library of system buttons exists to create the standardized, predefined buttons you see in many different applications. You also create system buttons as UIBarButtonItem objects, using the class's initWithBarButtonSystemItem method. An example follows:

UIBarButtonItem *myBookmarks = [ [ UIBarButtonItem alloc ]
    initWithBarButtonSystemItem: UIBarButtonSystemItemBookmarks
    target: self
    action: @selector(mySelector:)
];

The following system buttons are presently supported, and can be found in the UIBarButtonItem.h header file.

Button identifier Description
UIBarButtonSystemItemDone A blue text button labeled "Done"
UIBarButtonSystemItemCancel A text button labeled "Cancel"
UIBarButtonSystemItemEdit A text button labeled "Edit"
UIBarButtonSystemItemSave A blue text button labeled "Save"
UIBarButtonSystemItemAdd An image button of a plus (+) sign
UIBarButtonSystemItemFlexibleSpace An empty, flexible space
UIBarButtonSystemItemFixedSpace An empty spacer
UIBarButtonSystemItemCompose An image button of a pen and paper
UIBarButtonSystemItemReply An image button of a reply arrow
UIBarButtonSystemItemAction An image button of an action arrow
UIBarButtonSystemItemOrganize An image button of a folder with a down arrow
UIBarButtonSystemItemBookmarks An image button of the bookmarks icon
UIBarButtonSystemItemSearch An image button of the spotlight icon
UIBarButtonSystemItemRefresh An image button of a circular refresh arrow
UIBarButtonSystemItemStop An image button of a stop X
UIBarButtonSystemItemCamera An image button of a camera
UIBarButtonSystemItemTrash An image button of a trash can
UIBarButtonSystemItemPlay An image button of a scrubber play icon
UIBarButtonSystemItemPause An image button of a scrubber pause icon
UIBarButtonSystemItemRewind An image button of a scrubber rewind icon
UIBarButtonSystemItemFastForward An image button of a scrubber f-forward icon

3.5.4.3. Custom view buttons

Like navigation bars, a button can also be rendered as a custom view class, which allows you to display any other kind of view object as a button:

UIBarButtonItem *customButton = [ [ UIBarButtonItem alloc ]
    initWithCustomView: myView ];

3.5.4.4. Creating the toolbar

Add each button you'd like to display to the buttons array you've created:

[ buttons addObject: buttonImage ];
[ buttons addObject: buttonText ];
[ buttons addObject: myBookmarks ];

Next, create a UIToolbar object and assign your array of buttons as the toolbar's list of items:

UIToolbar *toolbar = [ [ UIToolbar alloc ] init ];
[ toolbar setItems: buttons animated: YES ];

Finally, replace the title view of your navigation bar with your newly created toolbar, just like you did with the segmented control:

self.navigationItem.titleView = toolbar;

When the navigation bar is displayed, the toolbar will appear in the center.

3.5.4.5. Sizing

The toolbar uses the default size for the buttons added to it. If you'd like to size the toolbar to fit the navigation bar more snugly, use the sizeToFit method:

[ toolbar sizeToFit ];

3.5.4.6. Toolbar style

As with many view-based objects, the UIToolbar includes a style property named barStyle. This can be used to match the toolbar style to the style you've defined for the navigation bar:

toolbar.barStyle = UIBarStyleDefault;

You can set the bar style to one of three standard style themes used by most other types of bar objects.

Style Description
UIBarStyleDefault Default style; gray background with white text
UIBarStyleBlackOpaque Solid black background with white text
UIBarStyleBlackTranslucent Transparent black background with white text

3.5.5. PageDemo: Page Navigation Exercise

In this example, you'll build a navigation controller on top of an existing view controller. The root view controller's navigation properties add a Credits button that, when pressed, pushes another view controller onto the navigation controller's stack. When the credits are displayed, pressing the Back button will pop the view controller off the stack, navigating back to the root view. A segmented control has also been added to the root view, allowing the user to choose between displaying text about bunnies versus text about ponies. The behavior is illustrated in Figure 3-2.

Figure 3-2. PageDemo example


You can compile this application, shown in Examples Example 3-17 through Example 3-21, with the SDK by creating a navigation-based application project named PageDemo. Be sure to pull out the Interface Builder code if you'd like to see how these objects are created from scratch.

Example 3-17. PageDemo application delegate prototypes (PageDemoAppDelegate.h)
#import <UIKit/UIKit.h>
#import "RootViewController.h"

@interface PageDemoAppDelegate : NSObject <UIApplicationDelegate> {
    UIWindow *window;
    RootViewController *viewController;
    CreditsViewController *creditsViewController;
    UINavigationController *navigationController;
}

@property (nonatomic, retain) IBOutlet UIWindow *window;
@property (nonatomic, retain) IBOutlet RootViewController *viewController;
@property (nonatomic, retain) IBOutlet CreditsViewController *creditsViewController;
@property (nonatomic, retain) IBOutlet UINavigationController *navigationController;

@end

                                          

Example 3-18. PageDemo application delegate (PageDemoAppDelegate.m)
#import "PageDemoAppDelegate.h"
#import "RootViewController.h"

@implementation PageDemoAppDelegate

@synthesize window;
@synthesize viewController;
@synthesize navigationController;
@synthesize creditsViewController;

- (void)applicationDidFinishLaunching:(UIApplication *)application {

    window = [ [ UIWindow alloc ] initWithFrame: [
        [ UIScreen mainScreen ] bounds ]
    ];

    viewController = [ [ RootViewController alloc ] initWithAppDelegate: self ];
    creditsViewController = [ [ CreditsViewController alloc ]
        initWithAppDelegate: self
    ];

    navigationController = [ [ UINavigationController alloc ]
        initWithRootViewController: viewController
    ];

    [ window addSubview: [ navigationController view ] ];
    [ window makeKeyAndVisible ];
}

- (void)dealloc {
    [ navigationController release ];
    [ viewController release ];
    [ creditsViewController release ];
    [ window release ];
    [ super dealloc ];
}

- (void)credits {
    [ navigationController pushViewController: creditsViewController
        animated: YES
    ];
}

- (void)back {
    [ navigationController popViewControllerAnimated: YES ];
}

@end

                                          

Example 3-19. PageDemo view controller prototypes (RootViewController.h)
#import <UIKit/UIKit.h>
#import <UIKit/UITextView.h>

@interface RootViewController : UIViewController {
    UITextView *textView;
    UIBarButtonItem *credits;
    UISegmentedControl *segmentedControl;
    UINavigationController *navigationController;
    int page;
}
- (void)setPage;
- (id)initWithAppDelegate:(id)appDelegate;

@end

@interface CreditsViewController : UIViewController {
    UITextView *textView;
    UINavigationController *navigationController;
}
- (id)initWithAppDelegate:(id)appDelegate;

@end

Example 3-20. PageDemo view controllers (RootViewController.m)
#import "RootViewController.h"
#import "PageDemoAppDelegate.h"

@implementation RootViewController

- (id)initWithAppDelegate:(id)appDelegate {
    self = [ super init ];
    if (self != nil) {

        /* Initialize Navigation Buttons */
        credits = [ [ [ UIBarButtonItem alloc ]
                  initWithTitle:@"Credits"
                  style: UIBarButtonItemStylePlain
                  target: appDelegate
                  action:@selector(credits) ]
                autorelease ];
        self.navigationItem.rightBarButtonItem = credits;

        segmentedControl = [ [ UISegmentedControl alloc ] initWithItems: nil ];
        segmentedControl.segmentedControlStyle = UISegmentedControlStyleBar;

        [ segmentedControl insertSegmentWithTitle: @"Bunnies" atIndex: 0
            animated: NO
         ];
        [ segmentedControl insertSegmentWithTitle: @"Ponies" atIndex: 1
            animated: NO
         ];

        [ segmentedControl addTarget: self action: @selector(controlPressed:)
             forControlEvents:UIControlEventValueChanged
         ];

        self.navigationItem.titleView = segmentedControl;
        segmentedControl.selectedSegmentIndex = 0;
    }
    return self;
}

- (void)controlPressed:(id) sender {
    [ self setPage ];
}

- (void)setPage {
    int index = segmentedControl.selectedSegmentIndex;

    if (index == 0) {
        textView.text = @"OMG Bunnies!";
    } else {
        textView.text = @"OMG Ponies";
    }
}

- (void)loadView {
    CGRect bounds = [ [ UIScreen mainScreen ] applicationFrame ];
    [ super loadView ];

    textView = [ [ UITextView alloc ] initWithFrame: bounds ];
    textView.editable = NO;

    [ self setPage ];
    self.view = textView;
}

- (void)dealloc {
    [ textView release ];
    [ super dealloc ];
}

@end

@implementation CreditsViewController

- (id)initWithAppDelegate:(id)appDelegate {

    self = [ super init ];
    if (self != nil) {

        /* Initialize Navigation Buttons */
        UIBarButtonItem *back = [ [ [ UIBarButtonItem alloc ]
                  initWithTitle:@"Back"
                  style: UIBarButtonItemStylePlain
                  target: appDelegate
                  action: @selector(back) ]
                autorelease ];
        self.navigationItem.backBarButtonItem = back;
    }
    return self;
}

- (void)loadView {
    [ super loadView ];

    textView = [ [ UITextView alloc ] initWithFrame: [
        [ UIScreen mainScreen ] applicationFrame ] ];
    textView.editable = NO;
    textView.text = @"iPhone SDK Application Development\n"
                     "Copyright (c) 2008, O'Reilly Media.";

    self.view = textView;
}

- (void)dealloc {
    [ textView release ];
    [ super dealloc ];
}

@end

                                          

Example 3-21. PageDemo main (main.m)
#import <UIKit/UIKit.h>

int main(int argc, char *argv[]) {

    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    int retVal = UIApplicationMain(argc, argv, nil, @"PageDemoAppDelegate");
    [pool release];
    return retVal;
}

                                          

3.5.6. What's Going On

The PageDemo example contains everything you've seen so far, and adds a navigation bar controller for an existing view controller to manage navigation between it and another view controller:

  1. The application is instantiated and the program's main function is called, which invokes the PageDemoAppDelegate class's applicationDidFinishLaunching method.

  2. The application delegate creates an instance of the root view controller and the credits view controller. A custom initialization method named initWithAppDele⁠gate is used to initialize the view controller and pass a pointer to the application delegate object, which will handle the actions for each button. A segmented control is also added.

  3. After the view controllers have finished initializing, the application delegate continues by instantiating a navigation controller object, attaching a view controller as its root. The navigation bar controller is then attached to the window as a subview, where it displays itself and the view controller it is wrapping.

  4. When a user taps the Credits button on the navigation bar, the button's action invokes the credits method in the application delegate class. This pushes the creditsViewController object onto the navigation controller, automatically transitioning the screen to the new view. When the user presses the Back button, the button's action invokes the back method in the application delegate class. This pops the controller off the stack, returning the iPhone's screen to the previous view controller.

  5. When the user taps a button on the segmented control, the selector method invokes the setPage method. The text that is assigned to the text view depends on whether the user selects "bunnies" or "ponies" in the segmented control.

3.5.7. Further Study

With the code from this section and the previous examples, try having a little fun with this example:

  • Try changing this code to add a third segment, "Hax" to the segmented control. If the user selects this segment, the text "OMG Hax!" should be displayed.

  • Take the UITextView code from previous examples and add two buttons, one for HTML and one for Text. Tapping each button should change the text view to display the file in the corresponding format.

  • Replace the segmented control with a toolbar. Add a UIBarButtonSystemItemAdd system button that, when tapped, makes the text view editable, and a UIBarButtonSystemItemDone button that, when tapped, makes the text view read-only again.

  • Add a third view controller displaying an HTML view of the text. Push this onto your navigation stack, and add the appropriate buttons to navigate back.

  • Check out the following prototypes in your SDK's header files: UINavigationCon⁠troller.h, UINavigationBar.h, and UIBarButtonItem.h. You'll find these under /Developer/Platforms/iPhoneOS.platform, inside the UI Kit framework's Headers directory.