This tutorial will
implement an app that displays a list of flowers by color, including
images for each row. It will also enable the user to touch a specific
flower and show a detail view. The detail view will load the content of a
Wikipedia article for the selected flower. The finished application
will resemble Figure 1.
Implementation Overview
The implementation of a navigation-based application is refreshingly simple. As a developer, the navigation controller (UINavigationController)
frees you up to focus on writing application functionality. When you
want a new view to appear, you simply “push” its view controller onto
the navigation controller’s stack. The new controller is instantiated
and added to the stack, and the previous controller gets pushed further
down the stack. When (and if) it is time to go back, the navigation
controller “pops” the current view off the stack, unloading it. The
previous view controller then moves to the top of the stack, becomes
active again, and the user can navigate to another item.
To manage the data, we’ll use a combination of NSMutableDictionaries and NSMutableArrays to store our data in a more easy-to-maintain format.
If you haven’t
encountered stacks before, don’t worry; you’ll catch on quickly. Imagine
creating a stack of papers on your desk. To add a page to the stack,
you “push” it onto the stack. You may only remove the top page at any
time, by “popping” it off. The more pages you push onto the stack of
paper, the more you’ll have to “pop” off to get back to the first page.
A navigation controller does exactly this but with view controllers/views rather than pieces of paper.
Preparing the Project
Instead of starting with the
Window-Based Application template, start Xcode and create a new project
using the Navigation-Based Application template. If you want to follow
along exactly with what we’re doing, name the project FlowerInfoNavigator.
The Navigation-Based
Application template does all the hard work of setting up a navigation
controller and an initial table-based view. This is the “heart and soul”
of many navigation-based applications and gives us a great starting
point for adding functionality.
Understanding the UINavigationController Hierarchy
After
creating the new project, click the Classes folder and review the
contents. You should see header and implementation files for the
application delegate (FlowerInfoNavigatorAppDelegate) and a subclass of UITableViewController called RootViewController. We will supplement this by creating a new detail view controller shortly.
Exploring the XIB files reveals an interesting hierarchy, as shown in Figure 2.
The MainWindow.xib file contains all the usual components but also a navigation controller (UINavigationController) with navigation bar (UINavigationBar). The controller provides the functionality to push and pop other view controllers, while the UINavigationBar instance creates the horizontal bar that will contain our UI elements for navigating through views.
Inside the navigation controller is the instance of the Root View Controller (a subclass of UITableViewController).
This is the top-level controller that is pushed onto the navigation
controller. A user cannot navigate back beyond this controller. (Note
that the table view itself is loaded RootViewController.xib.)
Finally, within the Root View Controller is a navigation item (UINavigationItem), which we will use to display a title in the navigation bar.
Feel free to build the app
and try it out. Even though we’re starting with an empty template,
you’ll still be able to see the navigation bar and the root table view.
Providing Data to the Application
In the previous table implementation project, we used multiple arrays and switch
statements to differentiate between the different sections of flowers.
This time, however, we need to track the flower sections, names, image
resources, and the detail URL that will be displayed.
Creating the Application Data Structures
What the application needs to store is quite a bit of data for simple arrays. Instead, we’ll make use of an NSMutableArray of NSMutableDictionaries
to hold the specific attributes of each flower and a separate array to
hold the names of each section. We’ll index into each based on the
current section/row being displayed, so no more switch statements!
To begin, edit RootViewController.h to read as seen in Listing 1.
Listing 1.
#import <UIKit/UIKit.h>
@class DetailViewController;
@interface RootViewController : UITableViewController { DetailViewController *detailViewController; NSMutableArray *flowerData; NSMutableArray *flowerSections; }
-(void) createFlowerData;
@end
|
We’ve added two NSMutableArrays: flowerData and flowerSection. These will hold our flower and section information, respectively. We’ve also declared a method createFlowerData, which will be used to add the data to the arrays.
Next, open the RootViewController.m implementation file and add the createFlowerData method shown in Listing 2.
Listing 2.
1: - (void)createFlowerData { 2: 3: NSMutableArray *redFlowers; 4: NSMutableArray *blueFlowers; 5: 6: flowerSections=[[NSMutableArray alloc] initWithObjects: 7: @"Red Flowers",@"Blue Flowers",nil]; 8: 9: redFlowers=[[NSMutableArray alloc] init]; 10: blueFlowers=[[NSMutableArray alloc] init]; 11: 12: [redFlowers addObject:[[NSMutableDictionary alloc] 13: initWithObjectsAndKeys:@"Poppy",@"name", 14: @"poppy.png",@"picture", 15: @"http://en.wikipedia.org/wiki/Poppy",@"url",nil]]; 16: [redFlowers addObject:[[NSMutableDictionary alloc] 17: initWithObjectsAndKeys:@"Tulip",@"name", 18: @"tulip.png",@"picture", 19: @"http://en.wikipedia.org/wiki/Tulip",@"url",nil]]; 20: 21: [blueFlowers addObject:[[NSMutableDictionary alloc] 22: initWithObjectsAndKeys:@"Hyacinth",@"name", 23: @"hyacinth.png",@"picture", 24: @"http://en.wikipedia.org/wiki/Hyacinth_(flower)", 25: @"url",nil]]; 26: [blueFlowers addObject:[[NSMutableDictionary alloc] 27: initWithObjectsAndKeys:@"Hydrangea",@"name", 28: @"hydrangea.png",@"picture", 29: @"http://en.wikipedia.org/wiki/Hydrangea", 30: @"url",nil]]; 31: 32: flowerData=[[NSMutableArray alloc] initWithObjects: 33: redFlowers,blueFlowers,nil]; 34: 35: [redFlowers release]; 36: [blueFlowers release]; 37: }
|
Don’t worry if you don’t understand what you’re seeing; an explanation is definitely in order! The createFlowerData method creates two arrays: flowerData and flowerSections.
The flowerSections
array is allocated and initialized in lines 6–7. The section names are
added to the array so that their indexes can be referenced by section
number. For example, Red Flowers is added first, so it is accessed by index (and section number!) 0, Blue Flowers is added second and will be accessed through index 1. When we want to get the label for a section, we can just reference it as [flowerSections objectAtIndex:section].
The flowerData structure is a bit more complicated. As with the flowerSections
array, we want to be able to access information by section. We also
want to be able to store multiple flowers per section and multiple
pieces of data per flower. So how can we get this done?
First, let’s concentrate on the individual flower data within each section. Lines 3–4 define two NSMutableArrays: redFlowers and blueFlowers. These need to be populated with each flower. Lines 12–30 do just that; the code allocates and initializes an NSMutableDictionary with key/value pairs for the flower’s name (name), image file (picture), and Wikipedia reference (url) and inserts it into each of the two arrays.
Wait a second, doesn’t this
leave us with two arrays when we wanted to consolidate all of the data
into one? Yes, but we’re not done. Lines 32–33 create the final flowerData NSMutableArray using the redFlowers and blueFlowers arrays. What this means for our application is that we can reference the red flower array as [flowerData objectAtIndex:0] and [flowerData objectAtIndex:1] (corresponding, as we wanted, to the appropriate table sections).
Finally, lines 35–36 release the temporary redFlowers and blueFlowers arrays. The end result will be a structure in memory that resembles Figure 3.
Populating the Data Structures
The createFlowerData method is now ready for use. We can call it from within the RootViewController’s viewDidLoad method. Because an instance of the RootViewController class is calling one of its own methods, it is invoked as [self createFlowerData]:
- (void)viewDidLoad {
[self createFlowerData];
[super viewDidLoad];
}
Remember, we need to release the flowerData and flowerSections when we’re done with them. Be sure to add the appropriate releases to the dealloc method:
- (void)dealloc {
[flowerData release];
[flowerSections release];
[super dealloc];
}
Adding the Image Resources
As you probably noticed when
entering the data structures, the application references images that
will be placed alongside the flower names in the table. In the project
files provided online, find the Flowers folder, select all the images,
and drag them into your Xcode resources folder for the project. If you
want to use your own graphics, size them at 100×75 pixels (and 200×150
for @2x iPhone 4 images), and make sure the names of the images stored with the picture NSMutableDictionary key match what you add to your project.
Creating the Detail View
The next task in developing
the application is building the detail view and view controller. This
view has a very simple purpose: It displays a URL in an instance of a UIWebView.
We automatically gain the ability to navigate back to the previous view
through the project’s navigation controller, so we can focus solely on
designing this view.
Creating a New View Controller
Begin by creating a new view controller called FlowerDetailViewController using the UIViewController subclass, as follows:
1. | In Xcode, choose File, New File, Cocoa Touch Class, and then the UIViewController subclass.
|
2. | Be sure to click the With XIB for user interface check box.
|
3. | |
4. | Make sure that Also Create FlowerDetailViewController.m is selected.
|
5. | Click Finish.
|
The implementation, header, and associated XIB for the new view controller will be added to the project.
Adding Outlets and Properties
The objects that we’ll
need to manipulate within the new detail view are very simple. There
will need to be an outlet for accessing the UIWebView
instance that we’ll add to the XIB in a bit, as well as a place for
storing and accessing the URL that the web view will display. We’ll call
these detailWebView and detailURL, respectively. Edit the FlowerDetailViewController header to read as shown in Listing 3.
Listing 3.
#import <UIKit/UIKit.h>
@interface FlowerDetailViewController : UIViewController { IBOutlet UIWebView *detailWebView; NSURL *detailURL; }
@property (nonatomic, retain) NSURL *detailURL; @property (nonatomic, retain) UIWebView *detailWebView;
@end
|
Remember to clean up by releasing the detailWebView and detailURL objects in the FlowerDetailViewController.m implementation file’s dealloc method:
- (void)dealloc {
[detailWebView release];
[detailURL release];
[super dealloc];
}
With this work out of the way, we can now implement the logic of the FlowerDetailViewController itself.
Implementing the Detail View Controller Logic
When the view is loaded, the UIWebView instance (detailWebView) should be instructed to load the web address stored within the NSURL object (detailURL).
We only have an NSURL (detailURL), we also need to use the NSURLRequest class method requestWithURL to return the appropriate object type. A single line of code takes care of all of this:
[detailWebView loadRequest:[NSURLRequest requestWithURL:detailURL]]
Add this to the viewDidLoad method in FlowerDetailViewController.m:
- (void)viewDidLoad {
[detailWebView loadRequest:[NSURLRequest requestWithURL:detailURL]];
[super viewDidLoad];
}
Adding the Web View in Interface Builder
Open the
FlowerDetailViewController.xib in Interface Builder. You should see a
single view in the Document window. We could replace this view entirely
with a web view, but by implementing the web view as a subview, we leave
ourselves a canvas for expanding the view’s interface elements in the
future.
Add a web view by opening the library (Tools, Library) and dragging a web view (UIWebView) onto the existing View icon. It should now appear as a subview within the view (see Figure 4).
Connect the web view to the detailWebView outlet by Control-dragging from the File’s Owner icon to the Web View icon in the Document window. When prompted, choose the detailWebView outlet, as demonstrated in Figure 5.
Congratulations, the detail view
is now finished! All that remains is providing data to the root view
table controller and invoking the detail view through the navigation
controller.
Implementing the Root View Table Controller
In the project template we’re working with, Apple has provided a table view controller called RootViewController for us to build from.
Even though we’re using a
navigation controller, very little changes between how we implemented
our initial table view controller and how we will be building this one.
Once again, we need to satisfy the appropriate data source and delegate
protocols to provide an interface to our data. We also need to react to a
row touch to drill down to our detail view.
The
biggest change to the implementation will be how we access our data.
Because we’ve built a somewhat complex structure of arrays of
dictionaries, we need to make absolutely sure we’re referencing the data
that we intend to.
Creating the Table View Data Source Methods
Instead of completely
rehashing the implementation details, let’s just review how we can
return the needed information to the various methods.
As with the previous example,
start by implementing the data source methods within
RootViewController.m. Remember, these methods (numberOfSectionsInTableView, tableView:numberOfRowsInSection, and tableView:titleforHeaderInSection) must return the number of sections, the rows within each section, and the titles for the sections, respectively.
To return the number of sections, we just need the count of the elements in the flowerSections array:
Retrieving the number of rows within a given section is only slightly more difficult. Because the flowerData
array contains an array for each section, we must first access the
appropriate array for the section, and then return its count:
[[flowerData objectAtIndex:section] count]
Finally, to provide the label for a given section via the tableView:titleforHeaderInSection method, the application should index into the flowerSections array by the section value and return the string at that location:
[flowerSections objectAtIndex:section]
Edit the appropriate methods in RootViewController.m so that they return these values.
Populating the Cells with Text and Images
The final mind-bending hurdle
that we need to deal with is how to provide actual content to the table
cells. As before, this is handled through the tableView:cellForRowAtIndexPath, but unlike the previous example, we need to dig down into our data structures to retrieve the correct results.
Recall that we will be setting the cell’s label using an approach like this:
[[cell textLabel]setText:@"My Cell Label"]
In addition to the label,
however, we also need to set an image that will be displayed alongside
the label in the cell. Doing this is very similar to setting the label:
[[cell imageView] setImage:[UIImage imageNamed:@"MyPicture.png"]]
To use our own labels and
images, however, things get a bit more complicated. Let’s quickly review
the three-level hierarchy of our flowerData structure:
flowerData(NSMutableArray)? → NSMutableArray → NSMutableDictionary
The first level, the top flowerData array, corresponds to the sections within the table. The second level, another array contained within the flowerData array, corresponds to the rows within the section, and, finally, the NSMutableDictionary provides the individual pieces of information about each row. Refer to Figure 13.12 if you’re still having trouble picturing how information is organized.
So, how do we get to the
individual pieces of data that are three layers deep? By first using the
section value to return the right array, and then, from that, using the
row value to return the right dictionary, and then finally, using a key
to return the correct value from the dictionary.
For example, to get the value that corresponds to the "name" key for a given section and row, we can write the following:
[[[flowerData objectAtIndex:indexPath.section] objectAtIndex: indexPath.row] objectForKey:@"name"]
Likewise, we can return the image file with this:
[[[flowerData objectAtIndex:indexPath.section] objectAtIndex: indexPath.row] objectForKey:@"picture"]
Substituting these values into the statements needed to set the cell label and image, we get the following:
[[cell textLabel] setText:[[[flowerData objectAtIndex:indexPath.section] objectAtIndex: indexPath.row] objectForKey:@"name"]]
and
[[cell imageView] setImage:[UIImage imageNamed:[[[flowerData objectAtIndex:indexPath.section] objectAtIndex: indexPath.row] objectForKey:@"picture"]]]
Add these lines to the tableView:cellForRowAtIndexPath method before the statement that returns the cell.
As a final decoration, the cell can display an arrow on the right side to show that it can
be touched to drill down to a detail view. This UI element is called a
“disclosure indicator” and can be added simply by setting the accessoryType property for the cell object:
cell.accessoryType=UITableViewCellAccessoryDisclosureIndicator
Add this line after your code to set the cell text and image. The table display setup is now complete.
Handling Navigation Events
Our implementation will need to create an instance of the FlowerDetailViewController and set its detailURL
property to the URL that we want the view to display. Finally, the new
view controller must be pushed onto the navigation controller stack.
Putting all these pieces together, the result is shown in Listing 4.
Listing 4.
1: - (void)tableView:(UITableView *)tableView 2: didSelectRowAtIndexPath:(NSIndexPath *)indexPath { 3: 4: FlowerDetailViewController *flowerDetailViewController = 5: [[FlowerDetailViewController alloc] initWithNibName: 6: @"FlowerDetailViewController" bundle:nil]; 7: flowerDetailViewController.detailURL= 8: [[NSURL alloc] initWithString: 9: [[[flowerData objectAtIndex:indexPath.section] objectAtIndex: 10: indexPath.row] objectForKey:@"url"]]; 11: flowerDetailViewController.title= 12: [[[flowerData objectAtIndex:indexPath.section] objectAtIndex: 13: indexPath.row] objectForKey:@"name"]; 14: [self.navigationController pushViewController: 15: flowerDetailViewController animated:YES]; 16: [flowerDetailViewController release]; 17: }
|
By the Way
The navigationController
instance that we’re using in this code was created by the application
template and is defined in the MainWindow.xib and application delegate
files. You don’t need to write any code at all to initialize or allocate
it.
Lines 4–6 allocate an instance of the FlowerDetailViewController and load the FlowerDetailViewController.xib file. Lines 7–8 set the detailURL property of the new detail view controller to the value of the dictionary key for the selected cell’s section and row.
The detail view controller instance, flowerDetailViewController, is now prepped and ready to be displayed. In lines 11–13, it is pushed on the navigation controller stack. Setting the animated parameter to "YES" implements a smooth sliding action onscreen.
Tweaking the Table UI
Before we can call this
application “done,” we need to make a few tweaks to the interface.
First, if you’ve run the app already, you know that the table view row
just isn’t large enough to accommodate the images that were provided.
Second, we need to set a title to be displayed in the navigation bar for
the initial table view. This title will then be used to create the
label in the “back” button on the subsequent detail view.
Changing the Row Size
To update the height of the
rows in the table, open the XIB file that defines the table view
(RootViewController.xib) in Interface Builder. Open the Document window
and make sure that the Table View icon is selected. Press Command+3 to
open the Size Inspector window. Update the row height size to at least
100 points, as shown in Figure 6.
Setting the Table Style
So far, both tables we’ve
created use the “plain” style. To change to the more rounded “grouped”
style, within the RootViewController.xib, select the Table View icon,
and open the Attributes Inspector. Use the Style drop-down menu to
switch between the Plain and Grouped options.
By the Way
If you set sizing information for one style of table, and then change the style, your previous size selections will be lost.
Setting a Navigation Bar Title
The title that appears in the navigation bar usually comes from a few different places. If a UINavigationItem
object exists in a view controller, the title property of that object
will appear as the label in the center of the navigation bar. If no UINavigationItem exists within the view controller, the controller’s title property is used as the navigation bar’s center label.
In this example application, the MainWindow.xib contains an instance of the table view controller (RootViewController) and a UINavigationItem.
To set a title that will appear in the navigation bar when the table
view is present (and also in the back button of the detail view), open
the MainWindow.xib and select the Navigation Item from the Document
window. With the item selected, press Command+1 to open the Attributes
Inspector. Enter an appropriate title into the Title field, such as Flower List.
Make sure you save all of your
files because you are finished! Build and run the FlowerInfoNavigator
application—try tapping through a few flowers. With a reasonably minor
amount of coding, we’ve created what feels like a very complex iPhone
application!