Navigation and view controller hierarchies

GFX9.COM share Navigation and view controller hierarchies, you can download now.



Navigation controllers are one of the primary tools for presenting multiple screens of content with the iOS SDK. This article will teach you how to do just that!


Introduction

As we saw in the previous lesson, UIKit's table view class is a great way to present tabular or columnar data. However, when content needs to be spread across multiple screens, a navigation controller, implemented in the UINavigationController class, is often the tool of choice.

Just like any other UIViewController subclass, a navigation controller manages a UIView instance. The navigation controller's view manages several subviews including a navigation bar at the top, a view containing custom content, and an optional toolbar at the bottom.

A navigation controller creates and manages a hierarchy of view controllers, which is known as a navigation stack. Once you understand how navigation controllers work they'll become easy to use.

In this article, we'll create a new iOS application to become familiar with the UINavigationController class. You'll notice that the combination of a navigation controller and a stack of (table) view controllers is an elegant and powerful solution.

In addition to the UINavigationController class, I will also cover the UITableViewController class, another UIViewController subclass. The UITableViewController class manages a UITableView instance instead of the default UIView instance. By default, it adopts the UITableViewDataSource and UITableViewDelegate protocols, which will save us quite a bit of time.


Another Project

The application that we are about to create is named Library. With our application, users can browse a list of authors and view the books they've written. The list of authors is presented in a table view.

When the user taps the name of an author, a list of books written by the author animates into view. Similarly, when the user selects a title from the list of books, another view animates into view, showing a fullscreen image of the book cover. Let's get started.

Creating the Project

Open Xcode, create a new project by selecting New > Project... from the File menu, and select the Single View Application template from the list of iOS templates.

Name the project Library, assign an organization name, and a company identifier. Choose a class prefix and set Devices to iPhone. Tell Xcode where you want to save the project and hit Create.

The template that we chose for this project contains an application delegate class (TPSAppDelegate), a storyboard, and a UIViewController subclass (TSPViewController).

Open TPSAppDelegate.m and take a look at the implementation of application:didFinishLaunchingWithOptions:. Its implementation should look familiar by now.

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    return YES;
}

Adding Resources

The source files provided with this article include the data that we will be using. You can find them in the folder named Resources. The folder includes a property list (Books.plist) containing information about the authors, the books they've written, and some information about each book, and an image for each book included in the property list.

Drag the Resources folder into your project to add them to the project. Xcode will show you a few options when you add the folder to the project. Make sure to check the checkbox labeled Copy items into destination group's folder (if needed) and don't forget to add the files to the Library target.

Property Lists

Before continuing, I want to take a moment to talk about property lists and what they are. A property list is nothing more than a representation of an object graph. As we saw earlier in this series, an object graph is a group of objects forming a network through the connections or references they share with each other.

It's easy to read and write property lists from and to disk, which makes them ideal for storing small amounts of data. When working with property lists, it's also important to remember that only certain types of data can be stored in property lists, such as strings, numbers, dates, arrays, dictionaries, and binary data.

Xcode makes browsing property lists very easy as we saw earlier. Select Books.plist from the Resources folder you added to your project and browse the contents of Books.plist using Xcode's built-in property list browser. This will be a helpful tool later in this article when we start working with the contents of Books.plist.

Subclassing UITableViewController

Before we can start using the data stored in Books.plist, we first need to lay some groundwork. This includes creating a view controller that manages a table view, which will display the authors listed in Books.plist.

Instead of using a UIViewController subclass and adding a table view to the view controller's view, as we did in the previous article, we'll create a UITableViewController subclass as I alluded to earlier in this article.

Start by removing TSPViewController.h and TSPViewController.m from your project. Create a new class by selecting New > File... from the File menu. Select the Objective-C class template in the list of iOS > Cocoa Touch templates. 

Name the new class TSPAuthorsViewController and make it a subclass of UITableViewController. There's no need to check the checkbox labeled Also create XIB file as we'll be using the storyboard to create the application's user interface.

Open Main.storyboard to replace the view controller in the storyboard with a table view controller. Select the view controller in the storyboard, hit the delete key, and drag a UITableViewController instance from the Object Library on the right. Select the new view controller, open the Identity Inspector on the right, and set its class to TSPAuthorsViewController.

Next, select the table view object in the workspace or from the list of objects on the left, open the Attributes Inspector on the right, and set Prototype Cells to 0. We won't be using prototype cells in this article.

Populating the Table View

Open TPSAuthorsViewController.m and inspect the file's contents. BecauseTPSAuthorsViewController is a subclass of UITableViewController, the implementation file is populated with default implementations of the required and a few optional methods of the UITableViewDataSource and UITableViewDelegate protocols.

Before we can display data in the table view, we need data to display. As I mentioned earlier, the property list Books.plist will serve as the data source of the table view. To use the data stored in Books.plist, we first need to load its contents into an object, an array to be precise.

Create a property of type NSArray in the view controller's header file and name it authors.

#import 

@interface TSPAuthorsViewController : UITableViewController

@property (strong, nonatomic) NSArray *authors;

@end

The view controller's viewDidLoad method is a good place to load the data from Books.plist into the view controller's authors property. We can do this using an NSArray class method, arrayWithContentsOfFile:.

self.authors = [NSArray arrayWithContentsOfFile:filePath];

The method accepts a file path, which means that we need to figure out what the file path of Books.plist is. The file, Books.plist, is located in the application bundle, which is a fancy name for the directory that contains the application executable and the application's resources—images, sounds, etc.

To obtain the file path of Books.plist, we first need a reference to the application's main bundle by using mainBundle, an NSBundle class method. The next step is to ask the application's bundle for the path of one of its resources, Books.plist. We do this by sending it a message of pathForResource:ofType: and passing the name and type (extension) of the file we want the path for. We store the file path in an instance of NSString as shown below.

NSString *filePath = [[NSBundle mainBundle] pathForResource:@"Books" ofType:@"plist"];

If we put the two pieces together, we end up with the following implementation of viewDidLoad. I also added an NSLog statement to log the contents of the authors property to the console so we can inspect its contents after loading the property list.

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSString *filePath = [[NSBundle mainBundle] pathForResource:@"Books" ofType:@"plist"];
    self.authors = [NSArray arrayWithContentsOfFile:filePath];
    NSLog(@"authors > %@", self.authors);
}

If you've read the previous article of this series, then populating the table view should be straightforward. The table view will contain only one section, which makes the implementation of numberOfSectionsInTableView: trivial.

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 1;
}

The number of rows in the only section of the table view is equal to the number of authors in the authors array so all we need to do is count the array's items.

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return [self.authors count];
}

The implementation of tableView:cellForRowAtIndexPath: is similar to the one we saw in the previous article. The main difference is how we fetch the data that we display in the table view cell.

The array of authors contains an ordered list of dictionaries, with each dictionary containing two key-value pairs. The object for the key named Author is an instance of NSString, whereas the object for the key Books is an array of dictionaries with each dictionary representing a book written by the author. Open Books.plist in Xcode to inspect the structure of the data source if this isn't clear.

With this information in mind, the implementation of tableView:cellForRowAtIndexPath: shouldn't be too difficult.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *CellIdentifier = @"Cell Identifier";
    
    [tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:CellIdentifier];
    
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
    
    // Fetch Author
    NSDictionary *author = [self.authors objectAtIndex:[indexPath row]];
    
    // Configure Cell
    [cell.textLabel setText:[author objectForKey:@"Author"]];
    
    return cell;
}

The keys of a dictionary are case sensitive so double-check the keys if you run into any problems. Build and run the project once more to see the result.


Adding a Navigation Controller

Adding a navigation controller is easy using a storyboard. However, before we add a navigation controller, it's important to understand how navigation controllers work on iOS.

Just like any other UIViewController subclass, a navigation controller manages a UIView instance. The navigation controller's view manages several subviews including a navigation bar at the top, a view containing custom content, and an optional toolbar at the bottom. What makes a navigation controller unique is that it manages a stack of view controllers.

Navigation Controllers and View Controller Hierarchies - The Navigation Stack of a Navigation Controller - Figure 11

The term stack can almost be taken literally. When a navigation controller is initialized, a root view controller is specified as we'll see in a moment. The root view controller is the view controller at the bottom of the navigation stack.

By pushing another view controller onto the navigation stack, the view of the root view controller is replaced with the view of the new view controller. When working with navigation controllers, the visible view is always the view of the topmost view controller of the navigation stack.

When a view controller is removed or popped from the navigation stack, the view of the view controller beneath it becomes visible once again. By pushing and popping view controllers onto and from a navigation controller's navigation stack, a view hierarchy is created and, as a result, a data hierarchy can be created. Let's see how all this pushing and popping works in practice.

Revisit application:didFinishLaunchingWithOptions: in the application delegate and instantiate a new navigation controller as shown below. We initialize the navigation controller using the initWithRootViewController: method and pass authorsViewController, which we created earlier, as the root view controller. By doing so, the authorsViewController becomes the first view controller of the navigation controller's navigation stack.

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Initialize Authors View Controller
    MTAuthorsViewController *authorsViewController = [[MTAuthorsViewController alloc] init];

    // Initialize Navigation Controller
    UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:authorsViewController];

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

    // Configure Window
    [self.window setRootViewController:navigationController];
    [self.window setBackgroundColor:[UIColor whiteColor]];
    [self.window makeKeyAndVisible];

    return YES;
}

Don't forget to set the rootViewController property of the application delegate's window property to the newly created navigation controller. Build and run the project. Can you spot the differences? When working with navigation controllers, we get a few things for free, such as the navigation bar at the top of the window. The navigation bar is an instance of the UINavigationBar class.

To add a title to the navigation bar, insert the following line into the viewDidLoad method of the MTAuthorsViewController class. Every view controller has a title property that is used in various places, such as the navigation bar. Build and run the project to see the result of this small change.

self.title = @"Authors";

Pushing and Popping

The next element to add to the application is the ability to see a list of books when the user taps the name of an author. This means that we need to capture the selection (the name of the author), instantiate a new view controller based on that selection, and push the new view controller onto the navigation stack to show it to the user. Does this sound complicated? It's not. Let me show you.

Another Table View Controller

Why not display the list of books in another table view. Create a new subclass of UITableViewController and name it MTBooksViewController (figure 12). Loading the list of books is easy as we saw earlier, but how does the books view controller know what author the user has selected? There are several ways to tell the new view controller about the user's selection, but the approach that Apple recommends is known as passing by reference. How does this work? The books view controller declares a property named author, which can be set to configure the books view controller to display the books of the selected author.

Navigation Controllers and View Controller Hierarchies - Creating Another Table View Controller Subclass - Figure 12

Open MTBooksViewController.h and add a property of type NSString and name it author as shown below. The nonatomic attribute that we pass in the property declaration is related to multithreading and data integrity. The nitty gritty details are not important for us at this point.

#import <UIKit/UIKit.h>

@interface MTBooksViewController : UITableViewController

@property (nonatomic) NSString *author;

@end

Remember from a few lessons ago that the advantage of Objective-C properties is that getter and setter methods for the corresponding instance variables are generated automatically for us. Sometimes, however, it can be necessary or useful to implement your own getter or setter method. This is one of those times. When the author property of the MTBooksViewController class is set, the data source of the table view needs to be modified. We will do this in the setter method of the _author instance variable. Let's see how this works.

Open MTBooksViewController.m and add a property to the @interface block at the top of the implementation file. You might be surprised to see an @interface block in the class's implementation file. This is known as a class extension and, as the name implies, it allows you to extend the interface of the class. The advantage of adding a class extension to the implementation file of a class is that the properties and instance variables you specify in a class extension are private. This means that they are only accessible by instances of the class. By declaring the books property in the class extension of MTBooksViewController, it can only be accessed and modified by an instance of the class. If an instance variable or property should only be accessed by instances of the class, then it is recommended to declare it as private.

@interface MTAuthorsViewController ()

@property NSArray *books;

@end

The setter method of the view controller's _author instance variable is pasted below. You can add it anywhere in the class' @implementation block. Don't be intimidated by the implementation. Let's start at the top. We first verify if the new value, author, differs from the the current value of _author. The underscore in front of author indicates that we are directly accessing the instance variable. The reasoning for this check is that it is (usually) not necessary to set the value of an instance variable if the value hasn't changed.

- (void)setAuthor:(NSString *)author {
    if (_author != author) {
        _author = author;

        NSString *filePath = [[NSBundle mainBundle] pathForResource:@"Books" ofType:@"plist"];
        NSArray *authors = [NSArray arrayWithContentsOfFile:filePath];

        for (int i = 0; i < [authors count]; i++) {
            NSDictionary *authorDictionary = [authors objectAtIndex:i];
            NSString *tempAuthor = [authorDictionary objectForKey:@"Author"];

            if ([tempAuthor isEqualToString:_author]) {
                self.books = [authorDictionary objectForKey:@"Books"];
            }
        }
    }
}

If the new value is different from the current value, then we update the value of the _author instance variable with the new value. A traditional setter usually ends here. However, we are implementing a custom setter method for a reason, that is, to dynamically set or update the books array, the data source of the table view.

The next two lines should be familiar. We load the property list from the application bundle and store its contents in an array named authors. We then iterate over the list of authors and search for the author that matches the author stored in _author. The most important detail of this setter is the comparison between tempAuthor and _author. If you were to use a double equal sign for this comparison, you would be comparing the references to the objects (pointers) and not the strings the objects are managing. The NSString class defines a method, isEqualToString:, that allows us to compare the strings instead of the object pointers.

For more information about setters and getters (accessors), I would like to refer to Apple's documentation. It is well worth your time to read this section of the documentation.

The rest of the MTBooksViewController class is easy compared to what we have covered so far. Take a look at the implementations of the three UITableViewDataSource protocol methods shown below.

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return [self.books count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *CellIdentifier = @"Cell Identifier";

    [tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:CellIdentifier];

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];

    // Fetch Books
    NSDictionary *book = [self.books objectAtIndex:[indexPath row]];

    // Configure Cell
    [cell.textLabel setText:[book objectForKey:@"Title"]];

    return cell;
}

Pushing a View Controller

When the user taps an author's name in the authors view controller, the application should show the list of books written by the author. This means that we need to instantiate an instance of the MTBooksViewController class, tell the instance what author was selected by the user, and push the new view controller onto the navigation stack to show it. This all takes place in the tableView:didSelectRowAtIndexPath: method of the authors view controller. The tableView:didSelectRowAtIndexPath: method is a method defined by the UITableViewDelegate protocol as we saw in the previous article about table views. First, however, we need to add an import statement at the top of MTAuthorsViewController.m to import the header file of the MTBooksViewController class.

#import "MTBooksViewController.h"

The implementation of tableView:didSelectRowAtIndexPath: is quite short. We initialize an instance of MTBooksViewController, set its author property, and push it onto the navigation stack by calling pushViewController:animated: on the navigation controller.

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    // Initialize Books View Controller
    MTBooksViewController *booksViewController = [[MTBooksViewController alloc] init];

    // Fetch and Set Author
    NSDictionary *author = [self.authors objectAtIndex:[indexPath row]];
    [booksViewController setAuthor:[author objectForKey:@"Author"]];

    // Push View Controller onto Navigation Stack
    [self.navigationController pushViewController:booksViewController animated:YES];
}

The second argument of pushViewController:animated: specifies whether the transition between view controllers should be animated (YES) or not (NO). You might be wondering how we have access to the navigation controller in the above code snippet? Whenever a view controller is pushed onto a navigation controller's navigation stack, the view controller keeps a reference to the navigation controller through the navigationController property, which is declared in the UIViewController class. You can browse the class reference of UIViewController to verify this.

Build and run the project. Tap the name of an author in the table view and observe how a new instance of the MTBooksViewController class is pushed onto the navigation stack and displayed to the user. Have you noticed that we also get a back button for free when using a navigation controller. The title of the back button is taken from the title of the previous view controller.


Adding a Book Cover

Whenever the user taps a book title in the books view controller, the application should show the book's cover. We won't be using a table view controller for this. Instead, we use a regular UIViewController subclass and display the book cover in an instance of the UIImageView class. The UIImageView class is a UIView subclass built for displaying images.

Create a new subclass of UIViewController (not UITableViewController) and name it UIBookCoverViewController. Make sure to check the checkbox labeled With XIB for user interface (figure 13).

Navigation Controllers and View Controller Hierarchies - Creating the MTBookCoverViewController Class - Figure 13

We need to add two properties to the new view controller's header file as shown below. The first property is of type UIImage and is a reference to the book cover that will be displayed in the image view. The second property is a reference to the image view that we will use to display the book cover. The IBOutlet keyword indicates that we will make the connection in Interface Builder. Let's do that now.

#import <UIKit/UIKit.h>

@interface MTBookCoverViewController : UIViewController

@property UIImage *bookCover;
@property IBOutlet UIImageView *bookCoverView;

@end

Open MTBookCoverViewController.xib and drag an instance of UIImageView from the Object Library to the view controller's view (figure 14). Make sure that the image view covers the entire view. Next, select the File's Owner object, open the Connections Inspector, and drag from the bookCoverView outlet to the UIImageView instance that we just added to the view controller's view (figure 15).

Navigation Controllers and View Controller Hierarchies - Adding an Image View - Figure 14
Navigation Controllers and View Controller Hierarchies - Connecting the Outlet - Figure 15

We could override the setter method of the _bookCover instance variable, but to show you an alternative approach, we set the image view's image property in the view controller's viewDidLoad method as shown below. We first check if the bookCover property is set (not nil) and then set the bookCoverView's image property to the value stored in the bookCover property.

- (void)viewDidLoad {
    [super viewDidLoad];

    if (self.bookCover) {
        [self.bookCoverView setImage:self.bookCover];
    }
}

Closing the Loop

All that is left for us to do, is show the book cover when the user tap's a book title in the books view controller. That means implementing the table view delegate method tableView:didSelectRowAtIndexPath: in the MTBooksViewController class. Don't forget to first import the header file of the MTBookCoverViewController class by adding an import statement at the top of MTBooksViewController.m.

#import "MTBookCoverViewController.h"

The differences with the implementation of tableView:didSelectRowAtIndexPath: in the MTAuthorsViewController class are small. Initializing an instance of the MTBookCoverViewController class is done by invoking initWithNibName:bundle: and passing the name of the nib file (without the xib extension) and a reference to the application bundle. We then fetch the book dictionary, create an image by passing the name of the image file to the imageNamed: class method of the UIImage class, and set the bookCover property of the new view controller, bookCoverViewController.

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    // Initialize Book Cover View Controller
    MTBookCoverViewController *bookCoverViewController = [[MTBookCoverViewController alloc] initWithNibName:@"MTBookCoverViewController" bundle:[NSBundle mainBundle]];

    // Fetch and Set Book Cover
    NSDictionary *book = [self.books objectAtIndex:[indexPath row]];
    UIImage *bookCover = [UIImage imageNamed:[book objectForKey:@"Cover"]];
    [bookCoverViewController setBookCover:bookCover];

    // Push View Controller onto Navigation Stack
    [self.navigationController pushViewController:bookCoverViewController animated:YES];
}

Finally, we push the view controller onto the navigation stack to show it to the user. Build and run the project to see the final result.

The UIImage class inherits directly from one of the Foundation root classes, NSObject. The UIImage class is more than a container for storing image data. UIImage is a powerful UIKit component for creating images from various sources including raw image data. The class also defines methods for drawing images with various options, such as blend modes and opacity values.


Where Does It Pop?

Earlier in this article, I explained that view controllers can be pushed onto and popped from a navigation stack. So far, we only explicitly pushed view controllers onto a navigation stack. Popping a view controller from a navigation stack happens when the user taps the back button in the navigation bar. This is another bit of functionality that we get for free.

However, chances are that you may need to manually pop a view controller from a navigation stack at some point. You can do so by sending the navigation controller a message of popViewControllerAnimated:, which will remove the topmost view controller from the navigation stack. Alternatively, you can pop all the view controllers from the navigation stack with the exception of the root view controller by sending the navigation controller a message of popToRootViewControllerAnimated:.


Conclusion

I hope you agree that navigation controllers aren't that complicated. To be honest, this article could have been much shorter, but I hope that you have learned a few more things in addition to working with navigation controllers. In the next article, we will take a look at tab bar controllers, which also allows you to manage a collection of view controllers.




submit to reddit


Similar content