Table-View-Based Applications - Learning iPhone Programming (2010)

Learning iPhone Programming (2010)

Chapter 5. Table-View-Based Applications

The UITableView and associated classes are perhaps the most commonly used classes when building UIs for your iPhone or iPod touch applications. Due to the nature of the applications, you can use these classes to solve a large cross section of problems, and as a result they appear almost everywhere. In this chapter, we’re going to dive fairly deeply into the table view classes, and by the end of it you’ll be able to produce UITableView-based applications on your own. We’ll also discuss some features of Xcode and Interface Builder as we go along.

We’re going to write a simple guidebook application. We’ll start by displaying a list of cities in a table (using the UITableView class). Then we’ll add the ability to click on the city name inside each table cell (each cell is a UITableViewCell object), which will take you to a page describing the city. Later in the chapter I’ll show you how to add and delete cities to and from the guidebook. By the end of the chapter, we will have a working guidebook application. However, before we get to write some new code, we’re going to do some helpful refactoring of the template code generated by Xcode.

Open Xcode and choose “Create a new Xcode project” in the startup window, and then choose the View-based Application template from the New Project pop-up window, the sample template we used for our Hello World application in Chapter 3. When prompted, name your new projectCityGuide.

Simplifying the Template Classes

One of the annoying things about the Xcode templates is the long class names Xcode chooses for the default classes. While the default class names are OK for small programs, they can become somewhat unwieldy, and at times rather inappropriate, when the amount of code you have increases. So, we’re going to modify the template Xcode provides before we add our own code, using a process known as refactoring the code.

Why are we doing this refactoring? Well, later in the chapter we’re going to be using more than one view controller inside the project. The original name of the default view controller created by the Xcode template would be somewhat misleading. In addition to changing its name to something that reflects its purpose, we will shorten the CityGuideAppDelegate name.

Open the CityGuideAppDelegate.h file, right-click on the CityGuideAppDelegate class name in the interface declaration, and select Refactor, as shown in Figure 5-1. This will bring up the Refactoring window. Let’s change the name of the main application delegate class fromCityGuideAppDelegate to CityGuideDelegate.

Select the class name, right-click, and select Refactor to access Xcode’s intelligent refactoring tool

Figure 5-1. Select the class name, right-click, and select Refactor to access Xcode’s intelligent refactoring tool

Note

Since Objective-C does not have namespaces, it’s a common practice to prefix your class names with initials to avoid namespace collision, or the situation where two classes have the same name but do different things. For instance, the Apple classes have the prefix NS for historical reasons, as Cocoa was based on the NeXTSTEP frameworks.

Entering the new class name and clicking Preview, as I’ve done in Figure 5-2, shows us that three files will be affected by the change. Click Apply and Xcode will propagate the changes throughout the project. Remember to save the affected files before you refactor the next set of classes.

The Refactoring window

Figure 5-2. The Refactoring window

Note

If you find that the File→Save menu is grayed out in Xcode, click on the file you want to save and then click somewhere inside the file (it doesn’t matter where). Then you’ll be able to save the file.

You should also change the name of the CityGuideViewController class. Open the CityGuideViewController.h file and right-click on the CityGuideViewController class name in the interface declaration, and again choose to refactor. Let’s change this class fromCityGuideViewController to RootController. Entering the new class name and clicking Preview shows that this change is more extensive, with six files being affected by the change. Click Apply, and the changes will again propagate throughout the project.

Notice, however, that Xcode has not changed the CityGuideViewController.xib file to be more appropriately named RootController.xib. We’ll have to make this change by hand. Click once on this file in the Groups & Files pane, wait a second, and click it again. You can then rename it to RootController.xib.

Unfortunately, since you had to make this change by hand, it hasn’t been propagated throughout the project. You’ll have to make some more manual changes. Double-click on the MainWindow.xib file to open it in Interface Builder. Click on the Root Controller icon in the main NIB window and open the Attribute pane of the Inspector window. As you can see in Figure 5-3, the NIB name associated with the root controller is still set as CityGuideViewController. Set this to RootController. You can either type the name of the controller into the window and Xcode will automatically perform name completion as you type, or use the control on the righthand side of the text entry box to get a drop-down panel where you’ll find the RootController class listed. Remember to save the NIB file using ⌘-S, and then test your refactoring by clicking the Build and Run (or depending on your Xcode setup, the Build and Debug) button in Xcode’s menu bar. You should see a bland gray screen pop up to prove that all is well.

Changing the NIB name from CityGuideViewController to RootController

Figure 5-3. Changing the NIB name from CityGuideViewController to RootController

Creating a Table View

With refactoring out of the way, now it’s time to put the UI together.

Double-click the RootController.xib file in Xcode to open it in Interface Builder. Then double-click on the View icon in the RootController.xib window to bring up the View window, and drag a table view from the Library window into the view. You’ll find the table view under Cocoa Touch→Data Views in the Library window.

Center the UITableView in the view, as shown in Figure 5-4. You must confirm that you’ve dropped it as a subview of the main view by clicking the View Mode widget on the menu bar of the RootController.xib window and choosing List View. It should look as shown in Figure 5-4, with Table View appearing under View. Save the .xib file using ⌘-S.

Dragging a UITableView from the Library window into the UIView

Figure 5-4. Dragging a UITableView from the Library window into the UIView

Switch back to Xcode to add the outlets and delegates Interface Builder needs so that you can connect the UITableView to your code. Open the RootController.h interface file and add a UITableView variable to the @interface declaration, then declare this as a property and anIBOutlet. You also need to declare that this class implements both the UITableViewDataSource and the UITableViewDelegate protocols. This means that it both provides the data to populate the table view and handles events generated by user interaction with the table view.

Once you’ve done this, the RootController.h file will look like this:

#import <UIKit/UIKit.h>

@interface RootController: UIViewController

<UITableViewDataSource, UITableViewDelegate>

{

UITableView *tableView;

}

@property (nonatomic, retain) IBOutlet UITableView *tableView;

@end

If you Option-double-click UITableViewDataSource in the declaration and then click the documentation icon in the upper-right corner of the window that appears (or ⌘-Option-double-click to go directly there), you’ll see that the protocol has a number of optional methods, as well as two mandatory methods (you must implement the methods that aren’t labeled as “optional”). Having declared that our view controller is a UITableViewDataSource, our RootController implementation must implement these two mandatory methods. These methods aretableView:cellForRowAtIndexPath: and tableView:numberOfRowsInSection:. The first of these methods returns a UITableViewCell object; the table view will ask the data source delegate for a cell each time a new cell is displayed in the view. The second method returns anNSInteger determining how many sections are in the table view. Table views can be divided into sections, and a title added to the top of each section. For now, we’ll use just one section (the default).

Despite what the documentation for UITableViewDelegate seems to suggest, there aren’t any mandatory methods. However, to obtain any sort of functionality from our table view we will at least have to implement the tableView:didSelectRowAtIndexPath: method.

Now we must add the implementation of those two mandatory data source methods to the RootController class (RootController.m). Once we have the code up and running we’ll look at the tableView:cellForRowAtIndexPath: method in detail. This method returns a populated table view cell for each entry (index) in the table, and it’s called each time the view controller wants to display a table view cell. For example, it’s called as the table view is scrolled and a new cell appears in the view.

Here are the contents of RootController.m. I marked in bold the lines I added to the file that the Xcode template generated:

#import "RootController.h"

@implementation RootController

@synthesize tableView;

#pragma mark Instance Methods

(void)didReceiveMemoryWarning {

// Releases the view if it doesn't have a superview.

[super didReceiveMemoryWarning];

// Release any cached data, images, etc that aren't in use.

}

- (void)viewDidUnload {

// Release any retained subviews of the main view.

// e.g. self.myOutlet = nil;

}

- (void)dealloc {

[tableView release];

[super dealloc];

}

#pragma mark UITableViewDataSource Methods

- (UITableViewCell *)tableView:(UITableView *)tv

cellForRowAtIndexPath:(NSIndexPath *)indexPath

{

UITableViewCell *cell =

[tv dequeueReusableCellWithIdentifier:@"cell"];

if( nil == cell ) {

cell = [[[UITableViewCell alloc]

initWithFrame:CGRectZero reuseIdentifier:@"cell"] autorelease];

}

return cell;

}

- (NSInteger)tableView:(UITableView *)tv

numberOfRowsInSection:(NSInteger)section

{

// Our table view will consist of only 3 cells

return 3;

}

#pragma mark UITableViewDelegate Methods

@end

Organizing and Navigating Your Source Code

I introduced something new in the preceding code listing: the #pragma mark declaration. If you examine the lower-righthand pane of the Xcode interface you’ll see that the title bar contains a filename, and immediately to the right of this is the name of the method inside which your cursor currently happens to be. If you click on this, you’ll see a drop-down menu showing all the method names in the implementation (you can access this menu easily using the Ctrl-2 keyboard shortcut). You’ll also see the text of the pragma marks I added to the code. For large classes, this is a convenient way to separate the methods involved in different jobs. In this case, I’ve added marks for the instance, data source, and delegate methods. You can also add a horizontal bar to the method list by adding the following:

#pragma mark -

Do not add a space after the -, as this will make Xcode think this is a text comment.

Connecting the Outlets

We now need to go back into Interface Builder and wire up the outlets to our code as we did in Connecting the Outlets in Interface Builder in Chapter 3. Open the RootController.xib file, and when Interface Builder opens, set the RootController.xib main window’s view mode to List, and then open the View list to reveal the table view.

Next, click the Table View icon and set the Inspector window to display the Connections Inspector (⌘-2). This reveals the dataSource and delegate outlets. Connect both of these to File’s Owner in the main window, which in this case is the RootController class, as shown inFigure 5-5.

Connecting the dataSource and delegate outlets of the UITableView in Interface Builder to the RootController class (File’s Owner)

Figure 5-5. Connecting the dataSource and delegate outlets of the UITableView in Interface Builder to the RootController class (File’s Owner)

Now click on the File’s Owner icon. In the outlets section of the Connections Inspector (⌘-2) you’ll see the tableView object that we flagged as an IBOutlet in the RootController.h file. Connect this with the UITableView as shown in Figure 5-6.

Note

If you don’t see the tableView object, quit Interface Builder (save your work so far), return to Xcode, and make sure you saved RootController.h. Then open RootController.xib in Interface Builder again; it should appear when you select the File’s Owner icon and go to the Connections Inspector.

Connecting the tableView IBOutlet in the RootController to the UITableView subview

Figure 5-6. Connecting the tableView IBOutlet in the RootController to the UITableView subview

We’ve reached a natural point at which to take a break. Quit Interface Builder (be sure to save any changes) and return to Xcode. The code should now run without crashing, although it’s not going to do very much. So, click Build and Run (or Build and Debug) to start the application in iPhone Simulator. Figure 5-7 shows what you should see.

OK, now we have the basic table view code working, so let’s go back to the RootController implementation (RootController.m) and look at that tableView:cellForRowAtIndexPath: method where we were creating and then returning table view cells. For performance reasons, the UITableView can reuse cells to enhance scroll performance by minimizing the need to allocate memory during scrolling. However, to take advantage of this ability we need to specify a reuse identifier string. The UITableView uses this to look up existing cells with the same identifier using the dequeueReusableCellWithIdentifier: method. If it can’t find an unused cell with the correct identifier, it will create one, but if an unused cell is available (perhaps it’s scrolled out of the current view), it will reuse it:

UITableViewCell *cell =

[tv dequeueReusableCellWithIdentifier:@"cell"];

if( nil == cell ) {

cell = [[[UITableViewCell alloc]

initWithFrame:CGRectZero reuseIdentifier:@"cell"] autorelease];

}

return cell;

The empty table view inside iPhone Simulator

Figure 5-7. The empty table view inside iPhone Simulator

So far our table view isn’t that interesting, so let’s push forward and add some content and some event handling. To do this, add an implementation for the tableView:didSelectRowAtIndexPath: delegate method to RootController.m. As the name suggests, this method is called when a user clicks on a table view cell. Because our cells are empty at the moment, we’ll also add some text to a cell before returning it from this method. Added lines of code are shown in bold:

#pragma mark UITableViewDataSource Methods

- (UITableViewCell *)tableView:(UITableView *)tv

cellForRowAtIndexPath:(NSIndexPath *)indexPath

{

UITableViewCell *cell =

[tv dequeueReusableCellWithIdentifier:@"cell"];

if( nil == cell ) {

cell = [[[UITableViewCell alloc]

initWithFrame:CGRectZero reuseIdentifier:@"cell"] autorelease];

}

cell.textLabel.text = @"Testing";1

return cell;

}

- (NSInteger)tableView:(UITableView *)tv

numberOfRowsInSection:(NSInteger)section

{

// Our table view will consist of only 3 cells

return 3;

}

#pragma mark UITableViewDelegate Methods

- (void)tableView:(UITableView *)tv2

didSelectRowAtIndexPath:(NSIndexPath *)indexPath

{

[tv deselectRowAtIndexPath:indexPath animated:YES];3

}

1

This is where we added text to the cell we’re returning from the tableView:cellForRowAtIndexPath: method.

2

Here’s where we implemented the tableView:didSelectRowAtIndexPath: delegate method.

3

Here we just told the table view to deselect the cell every time the user touches it and selects it. Because the animated argument is set to YES, the cell fades out as it deselects itself. Previously, if you touched the cell it would have stayed permanently selected.

You can see the results of these additions in Figure 5-8.

The new code running inside iPhone Simulator

Figure 5-8. The new code running inside iPhone Simulator

Building a Model

At this point, you should have a working UITableView. So far, you’ve implemented both the view and the controller parts of the MVC pattern. Now we’re going to return to Xcode and implement the model. This needs to be separate from the view and the view controller, since we want to decouple the way the data is stored from the way it is displayed as much as possible. This will increase the reusability of both the classes that handle the UI and the classes that store the data behind the scenes, allowing us to change how parts of the application work while affecting as little code as possible.

Right-click on the Classes folder in the Groups & Files pane and select Add→New File. When you see the New File window shown in Figure 5-9, make sure Cocoa Touch Class is selected on the left side of the screen. Next, select “Objective-C class,” make sure Subclass of NSObject is specified, and click on Next.

The New File window, which allows you to select the template Xcode will use to generate the new class interface and implementation file

Figure 5-9. The New File window, which allows you to select the template Xcode will use to generate the new class interface and implementation file

You will then be asked for the filename of the new class. Type in City.m and click on Finish. Xcode will generate a pair of files, City.h and City.m, containing the template interface and the implementation of the new class, and will put them in the Classes folder. If you look at these files, you can see that since you specified that the class was a subclass of the base NSObject class, Xcode really hasn’t created a lot of code. It didn’t know what you wanted the object for, so you’re going to have to write some code.

Open the City.h file and add variables to hold the name of our city, a short descriptive paragraph, and an image. Declare these variables as properties.

#import <Foundation/Foundation.h>

@interface City : NSObject {

NSString *cityName;

NSString *cityDescription;

UIImage *cityPicture;

}

@property (nonatomic, retain) NSString *cityName;

@property (nonatomic, retain) NSString *cityDescription;

@property (nonatomic, retain) UIImage *cityPicture;

@end

I’m declaring the name and description as an NSString, and I’m declaring the variable used to hold the picture as a UIImage. UIImage is a fairly high-level class that can be directly displayed in a UIImageView that we can create inside Interface Builder.

Note

I could have decided to use an NSMutableString rather than an NSString. An NSMutableString is a subclass of NSString that manages a mutable string, which is a string whose contents can be edited. Conversely, an NSString object manages an immutable string, which, once created, cannot be changed and can only be replaced. Using mutable strings here might give us a bit more flexibility later on, and if you decide you need it, you can always go back and change these definitions to mutable strings later. Changing from using anNSString to an NSMutableString is easy since mutable strings are a subclass and implement all of the methods provided by the NSString class. Going in the opposite direction is more difficult, unless you have not made use of the additional functionality offered by the mutable string class.

Open the City.m file and add code to @synthesize the cityName, cityDescription, and cityPicture accessor methods. After doing that, add a dealloc: method so that the variables will be released when the class is destroyed. Here’s what your City.m file should contain:

#import "City.h"

@implementation City

@synthesize cityName;

@synthesize cityDescription;

@synthesize cityPicture;

-(void) dealloc {

[cityName release];

[cityDescription release];

[cityPicture release];

[super dealloc];

}

@end

Because we made use of properties, our accessor methods will be generated for us automatically. So, we’re done now. Admittedly, this is just a fairly small class to hold some data, but it illustrates how useful properties will be for larger, more complex classes.

Let’s go back to the CityGuideDelegate class and prepopulate it with a few cities. You can put in longer descriptions if you want. If you’re just using it for personal testing, you could use text and images from Wikipedia. Later in the book I’ll show you how to retrieve data like this directly from the network, but for now we’ll hardcode (embed the data directly into the code; you normally will store your data outside the app) a few cities into the app delegate class and include the images inside the application itself rather than retrieving them from the network. Here’s what theCityGuideDelegate.h file should look like now (added lines are shown in bold):

#import <UIKit/UIKit.h>

@class RootController;1

@interface CityGuideDelegate : NSObject <UIApplicationDelegate> {

UIWindow *window;

RootController *viewController;

NSMutableArray *cities;

}

@property (nonatomic, retain) IBOutlet UIWindow *window;

@property (nonatomic, retain) IBOutlet RootController *viewController;

@property (nonatomic, retain) NSMutableArray *cities;

@end

1

You should #import the header files for any classes you’re using in the implementation. But if you need to reference a class in your header file, you should use the @class forward declaration instead of importing the class header file. Apple says in its documentation that the @classdirective “minimizes the amount of code seen by the compiler and linker, and is therefore the simplest way to give a forward declaration of a class name. Being simple, it avoids potential problems that may come with importing files that import still other files.”

In the application delegate interface file we declare our City class using the @class declaration, create an NSMutableArray to hold our list of cities, and declare this mutable array to be a property.

The changes to the application delegate implementation are slightly more extensive:

#import "CityGuideDelegate.h"

#import "RootController.h"

#import "City.h";1

@implementation CityGuideDelegate

@synthesize window;

@synthesize viewController;

@synthesize cities;2

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

City *london = [[City alloc] init];3

london.cityName = @"London";

london.cityDescription =

@"The capital of the United Kingdom and England.";

london.cityPicture = [UIImage imageNamed:@"London.jpg"];

City *sanFrancisco = [[City alloc] init];

sanFrancisco.cityName = @"San Francisco";

sanFrancisco.cityDescription = @"The heart of the San Francisco Bay Area.";

sanFrancisco.cityPicture = [UIImage imageNamed:@"SanFrancisco.jpg"];

City *sydney = [[City alloc] init];

sydney.cityName = @"Sydney";

sydney.cityDescription = @"The largest city in Australia.";

sydney.cityPicture = [UIImage imageNamed:@"Sydney.jpg"];

City *madrid = [[City alloc] init];

madrid.cityName = @"Madrid";

madrid.cityDescription = @"The capital and largest city of Spain. ";

madrid.cityPicture = [UIImage imageNamed:@"Madrid.jpg"];

self.cities = [[NSMutableArray alloc]

initWithObjects:london, sanFrancisco, sydney, madrid, nil]; 4

[london release]; 5

[sanFrancisco release];

[sydney release];

[madrid release];

// Override point for customization after app launch

[window addSubview:viewController.view];

[window makeKeyAndVisible];

}

- (void)dealloc {

[viewController release];

[window release];

[cities release]; 6

[super dealloc];

}

@end

1

First, we imported the City.h interface file.

2

Next, we synthesized the cities property to automatically create the accessor methods.

3

Following this, we declared and populated four instances of the City class. For each one, we allocated and initialized the instance object and then used the accessor methods to populate the instance. We could also have written an initWithName:withDescription:andImage:method for the class and achieved the same result by using this method to initialize the class. However, I do not discuss that sort of approach to class initialization until later in the book; the first time you’ll meet this is when I talk about web views near the start of Chapter 7.

4

Here, we initialized an NSMutableArray and populated it with the four cities. The trailing nil in the object list passed to the initWithObjects: method is essential. You must ensure that the last object in a comma-separated list of objects is the nil object; otherwise, when iterating through the array your code will end up pointing to unallocated memory, which will lead to an exception.

5

Here, we released the initial reference to each of the four instances of the City class. You’ll notice that previous to this we assigned references to these instances to the self.cities array. Remembering our discussion of the alloc-retain-release cycle in Chapter 4, we are therefore safe to reduce the count by releasing the initial reference, which reduces the reference count from two back to one, as adding an object to an array will increase its retain count. The memory allocated to these objects will therefore now be released when the self.cities array is released.

6

Here, we released the reference to the object instances that we are responsible for before releasing the reference to the class itself. If we did not do this, memory allocated for these objects would not be released until the application itself terminated. When this happens, especially with many objects, the condition is referred to as a memory leak.

Warning

If you create a UIImage using the imageNamed: method as shown in this example, it is added to the default autorelease pool rather than the event loop autorelease pool. This means the memory associated with such images will be released only when the application terminates. If you use this method with many large images, you’ll find that your application may quickly run out of memory. Since these images are part of an autorelease pool, you’ll be unable to free the memory they use when the device’s operating system calls thedidReceiveMemoryWarning: method in the application delegate when it runs short on memory. You should use the imageNamed: method sparingly, and generally only for small images.

Adding Images to Your Projects

As you can see, we retrieve the UIImage by name using the imageNamed: class method, but from where are we retrieving these images? The answer is, from somewhere inside the application itself. For testing purposes, I sorted through my image collection, found a representative image for each city (and then scaled and cropped the images to be the same size [1,000×750 pixels] and aspect ratio using my favorite image editing software), and copied them into the Xcode project. To do this yourself, drag and drop each image into the Resources folder in the Groups & Files pane. This brings up the copy file drop-down pane, as shown in Figure 5-10. If you want to copy the file into the project’s directory rather than create a link to wherever the file is stored, click on the relevant checkbox. If you do not copy the files to the project’s directory, they will still be collected in the application bundle file when Xcode compiles the application; however, if you later move or delete the file, Xcode will lose the reference to it and will no longer be able to access it. This is especially important when copying source code files. In general, I advocate always checking the box and copying the file into your project, unless you have a very good reason not to do so.

Note

There are other ways to add a file to a project. You can also right-click on the Resources folder and select Add→Existing Files to add a file to the project.

After you copy the downloaded images into the project, they become accessible from your code (see Figure 5-11). It’s generally advisable not to copy large images into the project. For example, if your binary gets too large you’ll have distribution problems. Among other problems, applications above a certain size cannot be downloaded directly from the App Store on the iPhone unless it is connected to the Internet via WiFi. Depending on the demographic you’re targeting, this may limit the market for your application. However, despite this, bundling images into your application is a good way to get easy access to small icons and logos that you may want to use in your project.

The drop down brought up when you drag and drop a file into the project

Figure 5-10. The drop down brought up when you drag and drop a file into the project

The downloaded images inside my Xcode project

Figure 5-11. The downloaded images inside my Xcode project

Connecting the Controller to the Model

Now that we’ve built the model, we have to go back to the RootController class and build the bridge between the view controller and the model. To do this we need to make only one change in the RootController interface declaration (RootController.h). Add a pointer to anNSMutableArray that you’ll then populate inside the viewDidLoad: method:

@interface RootController : UIViewController

<UITableViewDataSource, UITableViewDelegate> {

UITableView *tableView;

NSMutableArray *cities;

}

Changes to the implementation (RootController.m) are only slightly more extensive. You need to #import both the City.h and CityGuideDelegate.h interface files, as you’ll be using both of these classes inside the updated implementation:

#import "RootController.h"

#import "CityGuideDelegate.h"

#import "City.h"

As I mentioned earlier, you must implement the viewDidLoad: method. This UIViewController method is called after the controller’s view is loaded into memory, and is the method we’ll normally use to set up things that the view needs. You’ll find that the Xcode template included a stub for viewDidLoad (not far from the #pragma mark-labeled instance methods), but it’s commented out (wrapped inside a comment, so it doesn’t compile). Replace it with the following (be sure to remove the /* and */ so that it’s no longer commented out):

- (void)viewDidLoad {

CityGuideDelegate *delegate =

(CityGuideDelegate *)[[UIApplication sharedApplication] delegate];

cities = delegate.cities;

}

Here, we acquired a reference to the application delegate by using the [[UIApplication sharedApplication] delegate] method call. Since this method returns a generic id object, we had to cast it to be a CityGuideDelegate object before assigning it. We then grabbed a pointer to the array of cities managed by the app delegate.

Since our code now declares a new variable, we also have to remember to release it in the dealloc: method:

- (void)dealloc {

[tableView release];

[cities release];

[super dealloc];

}

Finally, we must use the model to populate the table view. The number of rows in the table view should now be determined by the number of cities in the NSMutableArray instead of simply returning “3” all the time. We must now go ahead and change the tableView: numberOfRowsInSection: method to reflect that by replacing the line return 3; (and the comment above it). Here’s how the method should look now:

- (NSInteger)tableView:(UITableView *)tv

numberOfRowsInSection:(NSInteger)section

{

return [cities count];

}

Finally, we need to change the tableView:cellForRowAtIndexPath: method to label the cell with the correct city name. To do this, add the following code shown in bold, which figures out which row of the table we’re trying to populate and looks up the appropriate element of thecities array:

- (UITableViewCell *)tableView:(UITableView *)tv

cellForRowAtIndexPath:(NSIndexPath *)indexPath

{

UITableViewCell *cell =

[tv dequeueReusableCellWithIdentifier:@"cell"];

if( nil == cell ) {

cell = [[[UITableViewCell alloc]

initWithFrame:CGRectZero reuseIdentifier:@"cell"] autorelease];

}

City *thisCity = [cities objectAtIndex:indexPath.row];

cell.textLabel.text = thisCity.cityName;

return cell;

}

We’ve now reached a point where we have a functional, buildable application. However, while our table view now reflects our model, we still can’t access any of the information we entered about our cities. When we click on a city we want the application to tell us about the city, and for that we need to modify the tableView:didSelectRowAtIndexPath: method. But for now, click the Build and Run button on the Xcode toolbar, and your iPhone Simulator should pop up, looking like Figure 5-12.

Mocking Up Functionality with Alert Windows

Before I go on to show how to properly display the city descriptions and images using the UINavigationController class, let’s do a quick hack and get the application to pop up an alert window when we click on a table view cell. Go back to RootController.m and add the highlighted lines in the following code to the didSelectRowAtIndexPath: method:

- (void)tableView:(UITableView *)tv

didSelectRowAtIndexPath:(NSIndexPath *)indexPath {

City *thisCity = [cities objectAtIndex:indexPath.row];

UIAlertView *alert = [[UIAlertView alloc]

initWithTitle:thisCity.cityName message:thisCity.cityDescription

delegate:self cancelButtonTitle:nil otherButtonTitles:@"OK", nil];

[alert show];

[alert autorelease];

[tv deselectRowAtIndexPath:indexPath animated:YES];

}

Populating the UITableView of our application using the new model

Figure 5-12. Populating the UITableView of our application using the new model

In this method, we create a UIAlertView window with an OK button, and set the title to be the city name and the contents to be the city description. You can see how this looks in Figure 5-13.

After you modify the tableView:didSelectRowAtIndexPath: method, a UIAlertView pop up appears when you click on a city name

Figure 5-13. After you modify the tableView:didSelectRowAtIndexPath: method, a UIAlertView pop up appears when you click on a city name

Adding Navigation Controls to the Application

Next, back out the changes you just made to the tableView:didSelectRowAtIndexPath: method by deleting the lines you added in the preceding section (be careful to not remove the call to deselectRowAtIndexPath).

Now, let’s wrap this app up properly. This means we have to add a UINavigationController to the application. If you’ve used many iPhone apps, you’ll be familiar with this interface; it’s one of the most commonly used iPhone design interface patterns. Clicking on a cell in the table view makes the current view slide to the left and a new view is displayed. You return to the original table view by clicking on the Back button.

The first thing you need to do is add an IBOutlet to a UINavigationController to the app delegate interface (CityGuideDelegate.h):

#import <UIKit/UIKit.h>

@class RootController;

@interface CityGuideDelegate : NSObject <UIApplicationDelegate> {

UIWindow *window;

RootController *viewController;

NSMutableArray *cities;

UINavigationController *navController;

}

@property (nonatomic, retain) IBOutlet UIWindow *window;

@property (nonatomic, retain) IBOutlet RootController *viewController;

@property (nonatomic, retain) IBOutlet UINavigationController *navController;

@property (nonatomic, retain) NSMutableArray *cities;

@end

You also need to make some modifications to the app delegate implementation (CityGuideDelegate.m). Add a new line of code near the top to @synthesize the new property:

@synthesize window;

@synthesize viewController;

@synthesize cities;

@synthesize navController;

Now you need to replace the section of the code that adds the RootController main view as a subview of the main window. Delete the following line from the bottom of the applicationDidFinishLaunching: method:

[window addSubview:viewController.view];

Next, replace it with the code shown in bold in the following code snippet. This new code adds the RootController to the NavController’s stack of view controllers, making its view the current view of the NavController. Then it sets the current NavController view as the subview of the main window. The end of the applicationDidFinishLaunching: method should look like this now:

// Override point for customization after app launch

navController.viewControllers = [NSArray arrayWithObject:viewController];

[window addSubview:navController.view];

[window makeKeyAndVisible];

}

As the current view of the NavController changes, it will automatically update the subview of the main window, and thus what the user sees on his screen. Let’s get this working first, and afterward I’ll discuss exactly how the NavController manipulates its stack of views.

Open the MainWindow.xib file in Interface Builder and drag and drop a navigation controller (UINavigationController) into the main NIB window (titled “MainWindow” or “MainWindow.xib”). The navigation controller is found on the Library (⌘-Shift-L) under Cocoa Touch→Controllers.

After doing so, you should see something similar to Figure 5-14. Note the navigation bar that appears at the top (with the title “City Guide”).

After adding the UINavigationController to the NIB, click on the CityGuide App Delegate icon in the main NIB window and switch to the Connections pane (⌘-2) of the Inspector window. Connect the navController outlet to the UINavigationController, as shown inFigure 5-15.

After performing this step, save the NIB file and return to Xcode. Open the RootController.m file and add the following snippet at the top of the viewDidLoad: method:

self.title = @"City Guide";

We’ve reached another good time to take a break, so click Build and Run. If you’ve followed all the steps, you should see what I see, something that looks a lot like Figure 5-16.

Adding a UINavigationController to the MainWindow.xib NIB file

Figure 5-14. Adding a UINavigationController to the MainWindow.xib NIB file

Connecting the UINavigationController to the outlet created in the application delegate code earlier

Figure 5-15. Connecting the UINavigationController to the outlet created in the application delegate code earlier

The CityGuide application is starting to look more like an iPhone application after adding a navigation bar

Figure 5-16. The CityGuide application is starting to look more like an iPhone application after adding a navigation bar

Adding a City View

You might have a nice navigation bar, but it doesn’t do any navigation yet, and after backing out of the changes you made to the tableView:didSelectRowAtIndexPath: method to present a pop up, the code doesn’t tell you about the selected city anymore. Let’s fix that now and implement a view controller and associated view to present the city information to the application user.

Right-click on the Classes folder in the Groups & Files pane and select Add→New File. Choose a UIViewController subclass and tick the checkbox to ask Xcode to generate an associated NIB file, as shown in Figure 5-17. When prompted, name the new class CityController.m, as this will be the view controller we’re going to use to present the information about our cities.

Select a UIViewController subclass and tick the checkbox for Xcode to create an associated XIB for the user interface

Figure 5-17. Select a UIViewController subclass and tick the checkbox for Xcode to create an associated XIB for the user interface

This will generate three new files: CityController.h, CityController.m, and CityController.xib. For neatness you might want to drag the CityController.xib file into the Resources folder of the project along with the other project NIB files.

Right now, the new NIB file is just a blank view. We’ll fix that later, but first we need to add code to the tableView:didSelectRowAtIndexPath: method in the RootController.m class to open the new view when a city is selected in the table view:

- (void)tableView:(UITableView *)tv

didSelectRowAtIndexPath:(NSIndexPath *)indexPath

{

CityGuideDelegate *delegate =

(CityGuideDelegate *)[[UIApplication sharedApplication] delegate];

CityController *city = [[CityController alloc] init];

[delegate.navController pushViewController:city animated:YES];

[city release];

[tv deselectRowAtIndexPath:indexPath animated:YES];

}

Here we grabbed a reference to the application delegate and initialized a new CityController instance. We then pushed this view controller onto the top of the UINavigationController stack, making its view the current view.

Additionally, at the top of the RootController.m class, since we’re now making use of the CityController class, we’ll also need to import its interface file into this class:

#import "CityController.h"

This is another good point to stop and try things out, so click the Build and Run button in the Xcode menu bar. If all has gone well, when you click on a city your table view should slide neatly to the left and reveal a blank white view created by the CityController view controller, with a navigation bar at the top and a Back button provided by your UINavigationController that will take you back to the city table view, as shown in Figure 5-18.

The blank view generated by the CityController view controller

Figure 5-18. The blank view generated by the CityController view controller

From here we need to modify the CityController class so that we can populate its view from the model held by the app delegate; then we need to build that view in Interface Builder by modifying the CityController.xib file. The first question we need to ask, however, is “How does the controller class know which city to display?” An easy way to make this happen is to override the init method. In the interface file (CityController.h), we’ll declare the following method:

- (id)initWithIndexPath:(NSIndexPath *)indexPath;

I plan to initialize the class by passing in the index (NSIndexPath) of the selected UITableViewCell in the main table view. From this you can figure out which City to use to populate the view. As you can imagine, this is one of a number of different ways to approach this problem.

In our view, we’ll be using the navigation bar to display the city name as the view title, a UITextView element to display the city description, and finally a UIImageView to display the picture of the city that we added to the project earlier. The interface file therefore has to declare these as variables and make them available to Interface Builder by also declaring them as an IBOutlet. Here’s what CityController.h should look like with these changes (including the line of code just listed):

#import <UIKit/UIKit.h>

@interface CityController : UIViewController {

NSIndexPath *index;

IBOutlet UIImageView *pictureView;

IBOutlet UITextView *descriptionView;

}

- (id)initWithIndexPath:(NSIndexPath *)indexPath;

@end

You’ll notice that we declared our variables as an IBOutlet inside the @interface declaration instead of doing so while declaring them as a property. There really isn’t any need to make these variables a property, as we don’t need accessor methods for them, and making the IBOutletdeclaration as part of the variable declaration is perfectly fine.

Note

Even when working with properties, you can put the IBOutlet declaration in the property’s variable declaration instead of the @property statement if you wish (it’s a matter of style).

I implemented the init method in CityController.m as follows:

- (id)initWithIndexPath: (NSIndexPath *)indexPath {

if ( self == [super init] ) {

index = indexPath;

}

return self;

}

This invokes the superclass init method and assigns the result to the self variable. If the call to the superclass is unsuccessful, self will be set to nil and this will be returned by the initWithIndexPath: method. This is very unlikely to occur, and if it does your application will crash. However, normally our line of custom initializer code will be executed: it sets the index variable to point to the NSIndexPath we passed into the object. We then initialize the view inside the viewDidLoad: method.

- (void)viewDidLoad {

CityGuideDelegate *delegate = (CityGuideDelegate *)

[[UIApplication sharedApplication] delegate];

City *thisCity = [delegate.cities objectAtIndex:index.row];

self.title = thisCity.cityName;

descriptionView.text = thisCity.cityDescription;

descriptionView.editable = NO;

pictureView.image = thisCity.cityPicture;

}

Inside the viewDidLoad: method we grabbed a reference to the application’s app delegate, and then used this and the index variable to retrieve the correct city. Then we set the text and image properties of the two subviews to hold the city data, and the title of the main view to be the city name. The title of the view will be displayed in the navigation bar. We also set the editable property of the descriptionView to NO, as we don’t want the user to be able to edit the text describing the city.

Since we’ve made use of both the CityGuideDelegate and the City classes in this method, we must also remember to import them in our implementation. Add these lines to the top of CityController.m:

#import "CityGuideDelegate.h"

#import "City.h"

Apart from the changes shown so far, the only other change to the default CityController implementation is to make sure we release our declared variables in the dealloc: method. Find the dealloc: method at the bottom of CityController.m and add the lines shown in bold:

- (void)dealloc {

[index release];

[descriptionView release];

[pictureView release];

[super dealloc];

}

Now we have to go back to the RootController implementation and make one quick change: substitute the new initWithIndexPath: method for the default init method call we originally used. In the tableView:didSelectRowAtIndexPath: method of RootController.m, replace the following line:

CityController *city = [[CityController alloc] init];

with this line, making use of the new initialization method:

CityController *city =

[[CityController alloc] initWithIndexPath:indexPath];

At this point, all we need to do is go into Interface Builder and build the view, and then connect the view to the outlets we declared and implemented inside the CityController class.

Opening the CityController.xib file in Interface Builder will present you with a blank view. Drag an image view (UIImageView) and text view (UITextView) element from the Library window (⌘-Shift-L) onto the view. These controls are available under Cocoa Touch→Data Views.

Since I resized my images to be the same aspect ratio, we’re going to change the size of our UIImageView to reflect that. In the Size tab of the Inspector window (⌘-3), resize the UIImageView to have a width of 250 pixels and a height of 188 pixels. Next, position it at X = 25 and Y = 37. Turning to the Attributes tab of the Inspector window (⌘-1), change the mode of the view to Aspect Fill. This means the image will be scaled to the size of the view, and if the aspect ratio of the image is not the same as the aspect ratio of the view, some portion of the image will be clipped so that the view is filled.

Turning to the UITextView element, use the Size tab of the Inspector window (⌘-3) to position it at X = 0 and Y = 223 with a width of W = 320 and a height of H = 256. This fills the main view below the image, as shown in Figure 5-19.

The CityController.xib with a UIImageView and UITextView added to the main view

Figure 5-19. The CityController.xib with a UIImageView and UITextView added to the main view

The only thing left to do is connect the UIImageView and UITextView elements to the two IBOutlet variables we created in code. In the main XIB window (titled CityController.xib), click on File’s Owner and go to the Connections tab in the Inspector window (⌘-2). Connect thedescriptionView outlet to the text view and the pictureView outlet to the image view, as shown in Figure 5-20.

Connecting the outlets to the UI views inside Interface Builder

Figure 5-20. Connecting the outlets to the UI views inside Interface Builder

At this point we’re done, so make sure the NIB file is saved and go back into Xcode and click the Build and Run button on the toolbar. After the application starts tap one of the city names and you should see something like Figure 5-21.

The city guide to <a href=London" width="479" height="911" border="0" />

Figure 5-21. The city guide to London

Edit Mode

So far, so good. But it would be nice if we could add more cities to our guide and, if we’re not interested in a particular city, delete it as well. Let’s implement a first cut at that using the UITableViewController edit mode. You’ll have seen this many times when using iPhone applications such as the Mail application. There is an Edit button on the top right on the navigation bar. When tapped, it will drop the table view into edit mode, allowing you to delete mail messages. In some applications, the Edit button lets you add entries to the table view.

This is such a commonly implemented pattern that there are hooks inside the UIViewController to simplify things. In the viewDidLoad: method of RootController.m, you need to add the following line of code:

self.navigationItem.rightBarButtonItem = self.editButtonItem;

This will add an Edit button to the navigation bar. Clicking on this button calls a method called setEditing:animated: on the view controller, which sets the table view into edit mode and changes the Edit button to a Done button. Clicking on the Done button will take the table view out of edit mode, and calls the setEditing:animated: method again, although this time to different effect (ending the edits and changing the button back to an Edit button).

Since we want to be able to add new cities, when the table view is put into editing mode we’re going to add another cell to our table view prompting us to “Add New City...”. When this is clicked, we’ll open a new view allowing us to enter the details of the city.

To do that we need to change the tableView:numberOfRowsInSection: method in RootController.m to return cities.count+1 when our table view has been put into editing mode. We’ll need to delete the one line (return cities.count;) in that method and replace it with the code shown in bold:

- (NSInteger)tableView:(UITableView *)tv

numberOfRowsInSection:(NSInteger)section {

NSInteger count = cities.count;

if(self.editing) {

count = count + 1;

}

return count;

}

We also need to edit the tableView:cellForRowAtIndexPath: method to return that extra cell when in edit mode:

- (UITableViewCell *)tableView:(UITableView *)tv

cellForRowAtIndexPath:(NSIndexPath *)indexPath {

UITableViewCell *cell =

[tv dequeueReusableCellWithIdentifier:@"cell"];

if( nil == cell ) {

cell = [[[UITableViewCell alloc]

initWithFrame:CGRectZero reuseIdentifier:@"cell"] autorelease];

}

if (indexPath.row < cities.count ) {

City *thisCity = [cities objectAtIndex:indexPath.row];

cell.textLabel.text = thisCity.cityName;

} else {

cell.textLabel.text = @"Add New City...";

cell.textLabel.textColor = [UIColor lightGrayColor];

cell.editingAccessoryType =

UITableViewCellAccessoryDisclosureIndicator;

}

return cell;

}

Next, we need to override the setEditing:animated: method to put the table view into edit mode and display the extra cell needed to prompt us to add a new city. Add this method to RootController.m somewhere above the #pragma mark–labeled UITableViewDataSourcemethods:

-(void)setEditing:(BOOL)editing animated:(BOOL) animated {

[super setEditing:editing animated:animated];

[tableView setEditing:editing animated:animated];

[tableView reloadData];

}

This code calls the super method and notifies the subview (the UITableView we are attempting to put into edit mode) that we have been put into edit mode. It then reloads the data in the table view to update the view the user sees.

By default, when you put the table view into edit mode, the edit control that appears next to the table view cell is of style UITableViewCellEditingStyleDelete, a red circle enclosing a minus sign, to signify that editing this row will delete the item in question. This is fine for our existing cities, but for the newly added “Add New City...” cell we need to set this to a different style. To do so, we need to implement the tableView:editingStyleForRowAtIndexPath: method that is part of the UITableViewDelegate protocol. This should go somewhere below the#pragma mark–labeled UITableViewDelegate methods:

- (UITableViewCellEditingStyle)tableView:(UITableView *)tv

editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath {

if (indexPath.row < cities.count ) {

return UITableViewCellEditingStyleDelete;

} else {

return UITableViewCellEditingStyleInsert;

}

}

In this method, we tell the table view that for cells occupied by existing cities we want the delete style; otherwise, we want the insert style, a green circle enclosing a plus sign.

Bearing in mind that we haven’t actually implemented the backend logic for editing yet, we’ve reached a good point to see if everything’s working. Click the Build and Run button in the Xcode toolbar, and when the City Guide application starts tap on the Edit button on the navigation bar. Your app should look just like Figure 5-22.

The City Guide table view in editing mode

Figure 5-22. The City Guide table view in editing mode

You’ve probably noticed that putting the table view into editing mode wasn’t really very pretty, as no animation was carried out while the table view reloaded the view. It’s actually fairly simple to change this by making use of two methods:insertRowsAtIndexPaths:withRowAnimation: and deleteRowsAtIndexPaths:withRowAnimation:.

Going back to our overridden setEditing:animated: method, we need to modify it to use these two methods as shown here:

-(void)setEditing:(BOOL)editing animated:(BOOL) animated {

if( editing != self.editing ) {

[super setEditing:editing animated:animated];

[tableView setEditing:editing animated:animated];

NSArray *indexes =

[NSArray arrayWithObject:

[NSIndexPath indexPathForRow:cities.count inSection:0]];

if (editing == YES ) {

[tableView insertRowsAtIndexPaths:indexes

withRowAnimation:UITableViewRowAnimationLeft];

} else {

[tableView deleteRowsAtIndexPaths:indexes

withRowAnimation:UITableViewRowAnimationLeft];

}

}

}

This code now checks to see whether we are changing editing modes; if we are, we call the super method and then notify our subview as before. However, instead of just calling [tableView reloadData] we now need to build an array containing the NSIndexPath of each cell we wish to insert (or delete) with animation. In our case, the array will hold only a single object since we intend to animate only a single cell; we then insert or delete with animation depending on whether we are entering or leaving editing mode, respectively.

After clicking Build and Run again, you should still see something that looks a lot like Figure 5-22; however, this time the “Add New City...” cell, as well as the + and – buttons, will be nicely animated and fly in and out. Note that you still won’t be able to do anything with these buttons, but at least they make a nice entrance and exit.

Deleting a City Entry

To actually delete a table view cell, we need to add the table view data source method tableView:commitEditingStyle:forRowAtIndexPath: to the code. Add this method to RootController.m between the #pragma mark-labeled UITableViewDataSource methods and the#pragma mark-labeled UITableViewDelegate methods:

- (void) tableView:(UITableView *)tv

commitEditingStyle:(UITableViewCellEditingStyle) editing

forRowAtIndexPath:(NSIndexPath *)indexPath {

if( editing == UITableViewCellEditingStyleDelete ) {

[cities removeObjectAtIndex:indexPath.row];

[tv deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]

withRowAnimation:UITableViewRowAnimationLeft];

}

}

In this method, we check that the editing style is set to delete, and if that’s the case, we remove the item from the cities array. We figure out which item to remove by checking the indexPath.row, and delete the relevant table view cell with animation.

You can now delete cities from the City Guide application. Click Build and Run and try it out. Tap the Edit button on the navigation bar, and then tap the edit control to the left of a city name. Tap the Delete button that appears, as shown in Figure 5-23. The city will be deleted. Tap the Done button.

Deleting a city

Figure 5-23. Deleting a city

The nice part about implementing things in this way is that you don’t have to drop the table into edit mode to delete a city; swiping from left to right will also bring up the Delete button.

Adding a City Entry

Before you can add a new city, you must implement an interface to allow the user to enter city metadata: the city name, a description, and an image. I’m going to put off adding the ability to add a picture to the city entry until the next chapter, where we look at various view controllers including the UIImagePickerController; for now, let’s implement the basic framework to allow us to add a new city by allowing the user to enter a city name and description.

Right-click on the Classes folder in the Groups & Files pane and select Add→New File. Choose a UIViewController class and tick the checkbox to ask Xcode to generate an associated XIB file, as shown in Figure 5-17. When prompted, name the new class AddCityController.m. You may want to drag the .xib file from the Classes group into the Resources group, just to keep things organized consistently.

As we did when we created the CityController class earlier, let’s add the hooks in the code which will allow us to open the new view when we click on the “Add New City...” cell after putting the table view into edit mode.

First we need to make some changes to the RootController class. Since we’re going to be using the new AddCityController class, we need to import the declaration into the implementation. Add this line to the top of RootController.m:

#import "AddCityController.h"

We also have to make some changes to the tableView:didSelectRowAtIndexPath: method in that same file:

- (void)tableView:(UITableView *)tv

didSelectRowAtIndexPath:(NSIndexPath *)indexPath

{

CityGuideDelegate *delegate =

(CityGuideDelegate *)[[UIApplication sharedApplication] delegate];

if (indexPath.row < cities.count && !self.editing ) {1

CityController *city =

[[CityController alloc] initWithIndexPath:indexPath];

[delegate.navController pushViewController:city animated:YES];

[city release];

}

if( indexPath.row == cities.count && self.editing ) {2

AddCityController *addCity = [[AddCityController alloc] init];

[delegate.navController pushViewController:addCity animated:YES];

[addCity release];

}

[tv deselectRowAtIndexPath:indexPath animated:YES];

}

1

We execute the commands within this if statement for cells whose row is less than the number of entries in the cities array, but only if the table view is not in editing mode.

2

We execute the commands within this if statement for cells whose row is equal to the number of entries in the cities array, but only if the table view is in editing mode.

Because Objective-C is derived from C, its array indexes start at zero. So, the only cell in our table view whose row number is greater than the number of entries in the city array is the “Add New City...” cell. Therefore, the code in the first if block uses the cities array to display each cell; the code in the second block uses a new city that the user is adding.

The first code branch, for city cells, is unchanged from the original implementation. While the second branch is very similar to the first, in this case we create an AddCityController instance rather than a CityController instance.

Click the Build and Run button on the Xcode toolbar. Running the application at this point shows us that we’ve forgotten something. Right now clicking on any of the table view cells when the table is in edit mode, including the “Add New City...” cell, doesn’t do anything, despite having implemented code inside the tableView:didSelectRowAtIndexPath: method.

You need to go back to the RootController.xib file inside Interface Builder, select the UITableView element, and in the Attributes tab of the Inspector window (⌘-1) tick the Allow Selection While Editing box.

If you rerun the application after setting this flag inside Interface Builder and click on a city cell when the table view is in edit mode, you should see that it is briefly selected and then deselected. Clicking on the “Add New City...” cell, however, should slide in a blank view: the one associated with the AddCityController.xib file.

However, the brief selection effect you get when you click on one of the normal city cells inside edit mode is annoying. These cells shouldn’t be selectable in edit mode, but unfortunately there isn’t a way to tell our table view that only the last cell is selectable. There are several ways to fool the user into thinking that this is the case, though. One of these is to extend our setEditing:animated method in the RootController class to set the selection style of these cells to UITableViewCellSelectionStyleNone when the table view is in edit mode, and then set the style back to UITableViewCellSelectionStyleBlue when we leave edit mode. The changes you need to make to the setEditing:animated: method in the RootController.m file are significant, so you can simply replace the method with the following:

-(void)setEditing:(BOOL)editing animated:(BOOL) animated {

if( editing != self.editing ) {

[super setEditing:editing animated:animated];

[tableView setEditing:editing animated:animated];

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

[indices autorelease];

for(int i=0; i < cities.count; i++ ) {1

[indices addObject:[NSIndexPath indexPathForRow:i inSection:0]];

}

NSArray *lastIndex = [NSArray

arrayWithObject:[NSIndexPath

indexPathForRow:cities.count inSection:0]];2

if (editing == YES ) {

for(int i=0; i < cities.count; i++ ) {3

UITableViewCell *cell =

[tableView

cellForRowAtIndexPath:[indices objectAtIndex:i]];

[cell setSelectionStyle:UITableViewCellSelectionStyleNone];

}

[tableView insertRowsAtIndexPaths:lastIndex

withRowAnimation:UITableViewRowAnimationLeft];

} else {

for(int i=0; i < cities.count; i++ ) {4

UITableViewCell *cell =

[tableView

cellForRowAtIndexPath:[indices objectAtIndex:i]];

[cell setSelectionStyle:UITableViewCellSelectionStyleBlue];

}

[tableView deleteRowsAtIndexPaths:lastIndex

withRowAnimation:UITableViewRowAnimationLeft];

}

}

}

1

Inside this loop, we build an NSMutableArray containing the NSIndexPath of all the cells where we want to modify the selection style, that is, normal cells that contain cities.

2

Here we build an NSArray containing the NSIndexPath of the final “Add New City...” cell.

3

We have just entered edit mode, so inside this loop we retrieve the UITableViewCell for each NSIndexPath in our array and set the selection style to “None”.

4

Leaving edit mode we do the opposite, and set the selection style back to the default for each cell in the array.

Build and run the application and you’ll see that this gets you where you want to go: inside edit mode the only (apparently) selectable cell is the “Add New City...” cell. None of the other cells show any indication that they have been selected when they are clicked on. However, outside edit mode these cells are selectable, and will take you (as expected) to the view describing the city.

The “Add New City...” Interface

There are a number of ways we could build an interface to allow the user to enter metadata about a new city. I’m going to take the opportunity to show you how to customize a UITableViewCell inside Interface Builder and load those custom cells into a table view.

Open the AddCityController.xib file in Interface Builder. Open the Library (⌘-Shift-L) and choose Cocoa Touch→Data Views. Drag and drop a table view (UITableView) into the view. Next, grab a UITableViewCell from the Library window and drag and drop that into the mainAddCityController NIB window (not the View window). Repeat this step and your AddCityController.xib window will look like Figure 5-24. Here you can see the main view with its table view and the two table view cells, which are not part of the main view. Double-clicking on a table view cell in this window will open the cell in a new view window. Each table view cell is a separate subview.

The main AddCityController NIB window in list view mode

Figure 5-24. The main AddCityController NIB window in list view mode

We now need to customize these two cells to give users the ability to enter text. To do this, we’re going to build a table view similar to the one Apple uses when we write a new mail message. Yes, in case you didn’t release it, that’s just a highly customized table view. It’s actually pretty amazing how far you can get writing iPhone applications just using the UITableView and associated classes.

Since you’re going to be using these cells to enter text, you don’t want them to be selectable, so you should open the Attributes tab for both of the cells (click on the cell’s name and press ⌘-1) and change the selection type from “Blue” to “None” in both cases.

At the top of the table view, we’ll have a normal-size table view cell with an embedded UITextField to allow users to enter the city name. Below that we’ll have a super-size table view cell with an embedded UITextView to allow users to enter the much longer description.

Double-click on the first of your two table view cells, grab a label (UILabel) from the Library window (Cocoa Touch→Inputs and Values), and drop it onto the Table View Cell window. Make sure the label is selected, and in the Attributes tab (⌘-1) of the Inspector window change the text to “City:”. Then switch to the Size tab (⌘-3) and position the label at X = 10 and Y = 11 with width W = 42 and height H = 21.

Now grab a Text field (UITextField) from the Library window and drag and drop it onto the same Table View Cell window. Click on the Text field, and in the Attributes tab of the Inspector window select the dotted-line border for the field. This represents the “no border” style. In the Text Input Traits section of the Attributes tab set Capitalize to “None” and Correction to “No”. With the Text field still selected, go to the Size tab of the Inspector window and resize the element to have origin X = 60 and Y = 0 with width W = 260 and height H = 44. In the Attributes tab you may want to add some placeholder text to the Text field to prompt the user to enter a city name. I went with “e.g. Paris, Rome”.

Next, double-click on the second of the two table view cells. You need to resize this to fill the remaining part of the main view. The navigation bar at the top of the view is 54 pixels high, and a standard table view cell is 44 pixels high. Since the iPhone’s screen is 460 pixels high, to fill the view we want the table view cell to be 362 pixels high. So, go to the Size tab in the Inspector window and set H = 362. The view window containing the table view cell will automatically grow to reflect its new size.

Warning

Apple explicitly warns developers in the documentation that we should not rely on the number of pixels in the iPhone screen staying constant. The next revision of the iPhone after the 3GS may have a larger screen, and our careful sizing of the table view cells based on the size of the screen in the current models will break our application’s interface. Instead of hardwiring the sizes of the table view cells as we have done here, in production code you should make use of the UIScreen class to determine the size of the main window. For more information, see Apple’s SDK documentation for UIScreen.

Grab another UILabel from the Library window and drop it onto the Table View Cell window. In the Attributes tab of the Inspector window change the text to “Description” and then switch to the Size tab. Position the label at X = 11 and Y = 1 with width W = 86 and height H = 21.

Now grab a UITextView from the Library window (Cocoa Touch→Data Views), drag and drop it into this new expanded table view cell, resize it to the remaining extent of the cell using the Size tab in the Inspector window (X = 11, Y = 29, W = 297, H = 332), and delete the default text from the Attributes tab. After doing so, you should have a collection of views that resembles that seen in Figure 5-25.

Finally, click on the UITextView, and in the View section of the Attributes tab (⌘-1) of the Inspector window set the Tag attribute to 777. Go to your other table view cell and do the same for its UITextField. The Tag attribute is a UIView property that Interface Builder is exposing to us; this is used to uniquely identify views (or in this case a subview) to our application. We’ll be able to grab the UITextView and UITextField easily using this tag directly from our code after setting it here in Interface Builder.

We’re done with Interface Builder for now, so save your changes to the NIB file, return to Xcode, and open the AddCityController.h file. Add the code shown in bold:

#import <UIKit/UIKit.h>

@interface AddCityController : UIViewController

<UITableViewDataSource, UITableViewDelegate> {

IBOutlet UITableView *tableView;

IBOutlet UITableViewCell *nameCell;

IBOutlet UITableViewCell *descriptionCell;

}

@end

Interface Builder with the two modified UITableViewCells

Figure 5-25. Interface Builder with the two modified UITableViewCells

Here we declare the view controller class to be both a data source and a delegate for the table view. We also declare three variables: a UITableView variable and two UITableViewCell variables. We declare each of these variables to be an IBOutlet; we’ll connect these variables to our views inside Interface Builder in a little while.

However, before we return to Interface Builder to do that, we need to implement a number of table view data source and delegate methods inside the AddCityController.m class implementation. Here is the full listing for that file:

#import "AddCityController.h"

@implementation AddCityController

#pragma mark ViewController Methods

- (void)didReceiveMemoryWarning {

// Releases the view if it doesn't have a superview.

[super didReceiveMemoryWarning];

// Release any cached data, images, etc that aren't in use.

}

- (void)viewDidLoad {

self.title = @"New City";1

}

- (void)viewDidUnload {

// Release any retained subviews of the main view.

// e.g. self.myOutlet = nil;

}

- (void)dealloc {2

[tableView release];

[nameCell release];

[descriptionCell release];

[super dealloc];

}

#pragma mark UITableViewDataSource Methods

- (UITableViewCell *)tableView:(UITableView *)tv

cellForRowAtIndexPath:(NSIndexPath *)indexPath

{

UITableViewCell *cell = nil;

if( indexPath.row == 0 ) {3

cell = nameCell;

} else {

cell = descriptionCell;

}

return cell;

}

- (NSInteger)tableView:(UITableView *)tv

numberOfRowsInSection:(NSInteger)section

{

return 2; 4

}

#pragma mark UITableViewDelegate Methods

- (CGFloat)tableView:(UITableView *)tv

heightForRowAtIndexPath:(NSIndexPath *)indexPath {

CGFloat height;

if( indexPath.row == 0 ) {5

height = 44;

} else {

height = 362;

}

return height;

}

@end

1

As we did for the CityController view controller, we need to add a title to the view inside the viewDidLoad: method. This title will be displayed in the navigation bar at the top of the view.

2

Since we have declared variables, we need to remember to release them inside the dealloc: method.

3

Instead of using the dequeueReusableCellWithIdentifier: method to obtain a cell, we check which row we are being asked to display, and return either our custom cell to enter the city name or the custom cell to enter the city description.

4

Since we have only two cells in our table view, we just return 2 from this method.

5

Since the table view cells are different heights, we have to return the correct height in pixels depending on which cell we are being asked about.

The only method you haven’t seen before is the tableView:heightForRowAtIndexPath: method. As you would expect, this delegate returns the height of the individual table view cell in a specified location.

Double-click the AddCityController.xib file to open it in Interface Builder. Click on File’s Owner and open the Connections tab (⌘-2) of the Inspector window. Connect the descriptionCell outlet to the super-size table view cell and the nameCell outlet to the smaller table view cell. If you aren’t sure which table view cell is which by looking at the AddCityController.xib window, you can open each one and drag the outlet to their open windows.

Finally, connect the tableView outlet to the table view in the main View window. Now click on the table view in the main View window and connect both the dataSource and the delegate outlets of the table view to File’s Owner. After doing this, click on File’s Owner, and the Connections tab of the Inspector window should look the same as in Figure 5-26.

We’ve reached a point where we should have a working application. Save the XIB, then click on the Build and Run button in the Xcode toolbar to compile and start the application in the simulator. Once it has started successfully, click on the Edit button to put your table view into edit mode and then click on the “Add New City...” cell. If everything has gone according to plan, you should see something like Figure 5-27.

The Connections tab of the Inspector window after making all the necessary connections inside Interface Builder between the various components

Figure 5-26. The Connections tab of the Inspector window after making all the necessary connections inside Interface Builder between the various components

The new “New City” UI in iPhone Simulator

Figure 5-27. The new “New City” UI in iPhone Simulator

If we tap inside one of the custom table view cells, the keyboard will appear and we can start typing. However, right now we don’t have any way to save the information we enter in these fields. Let’s implement that right now.

Capturing the City Data

Both the UITextField and the UITextView we’re using to capture the name and description of the city have delegate protocols. However, we don’t need to look into those quite yet, although I will be talking about them later in the book. The first step is to add a Save button to the interface.

That’s actually pretty easy to do from the viewDidLoad: method of the view controller. We can use the same technique we used to add the Edit and Done buttons to the main view controller to add our Save button to the AddCityController. However, instead of declaring the navigation item to be self.editButtonItem, we make use of the UIBarButtonItem method’s initWithBarButtonSystemItem:target:action: to create the navigation item:

self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc]

initWithBarButtonSystemItem:UIBarButtonSystemItemSave

target:self action:@selector(saveCity:)];

We must add the preceding code to the viewDidLoad: method of AddCityController.m. In this method call, we declare that the button delegate is self (the AddCityController class) and that, when clicked, the event will be handled by the saveCity: method in this class, which is shown next. We must add the following to AddCityController.m. This should go directly below the #pragma mark–labeled instance methods:

- (void)saveCity:(id)sender {

CityGuideDelegate *delegate =

(CityGuideDelegate *)[[UIApplication sharedApplication] delegate];

NSMutableArray *cities = delegate.cities;1

UITextField *nameEntry = (UITextField *)[nameCell viewWithTag:777]; 2

UITextView *descriptionEntry =

(UITextView *)[descriptionCell viewWithTag:777];

if ( nameEntry.text.length > 0 ) {3

City *newCity = [[City alloc] init];

newCity.cityName = nameEntry.text;

newCity.cityDescription = descriptionEntry.text;

newCity.cityPicture = nil;

[cities addObject:newCity];

RootController *viewController = delegate.viewController;

[viewController.tableView reloadData]; 4

}

[delegate.navController popViewControllerAnimated:YES]; 5

}

1

This gets a pointer to the cities array (the data model) held by the application delegate class.

2

Here the xxxTag property is used to obtain references to the UITextField and UITextView in the two custom table view cells.

3

If the city name text field holds some text, we must assume there is a new city to add to the guide. We need to create a new City object, populate it, and push it onto the cities array.

4

Because we have changed the size of the cities array, we need to reload the data held by the main view controller. The current view held by the object is not correct anymore.

5

We are done with this view, so we ask the navigation controller to remove it from its stack of views. This will mean that the current (displayed) view becomes the next view down in the navigation controller’s stack of views. In this specific case, this will be our previous view.

Since we’re making use of the CityGuideDelegate, RootController, and City classes in this method we must also remember to import their definitions into our implementation. Add these lines to the top of AddCityController.m:

#import "CityGuideDelegate.h"

#import "RootController.h"

#import "City.h";

We could actually compile and run the application at this point and it would work, mostly. But there are a few UI loose ends we need to clear up before everything works correctly.

When we click the Save button and return to the main table view, we will be reusing the table view cell which previously held the “Add New City...” cell to hold a city name in the newly expanded list of cities. This will cause some problems: while we explicitly set the color and accessory properties for this cell in cellForRowAtIndexPath: we don’t do the same for the other cells. We therefore have to make a small change to the tableView:cellForRowAtIndexPath: method and set the textLabel.textColor and editingAccessoryType for the other cells as well as the “Add New City...” cell. Make the changes shown here to the tableView:cellForRowAtIndexPath: method in RootController.m:

- (UITableViewCell *)tableView:(UITableView *)tv

cellForRowAtIndexPath:(NSIndexPath *)indexPath

{

UITableViewCell *cell =

[tv dequeueReusableCellWithIdentifier:@"cell"];

if( nil == cell ) {

cell = [[[UITableViewCell alloc]

initWithFrame:CGRectZero reuseIdentifier:@"cell"] autorelease];

}

NSLog( @"indexPath.row = %d, cities.count = %d",

indexPath.row, cities.count );

if (indexPath.row < cities.count ) {

City *thisCity = [cities objectAtIndex:indexPath.row];

cell.textLabel.text = thisCity.cityName;

cell.textLabel.textColor = [UIColor blackColor];

cell.editingAccessoryType = UITableViewCellAccessoryNone;

if (self.editing) {

[cell setSelectionStyle:UITableViewCellSelectionStyleNone];1

}

} else {

cell.textLabel.text = @"Add New City...";

cell.textLabel.textColor = [UIColor lightGrayColor];

cell.editingAccessoryType =

UITableViewCellAccessoryDisclosureIndicator;

}

return cell;

}

1

Since we are creating an extra cell while in edit mode, and as the table view has been flagged as allowing selection in edit mode, the selection style for this cell will be the default. The selection style will not be set implicitly since the setEditing:animated: method has already been called on this table view. We therefore have to set the selection style explicitly, to “None”, as the table view is already in edit mode when we return to it from the Add City view and the cell is created.

We’re done! Click the Build and Run button on the Xcode toolbar to compile and start the application in the simulator. Once it has started, click on the Edit button to put the table view into edit mode and then click on the “Add New City...” cell. Enter a name for the new city, as shown inFigure 5-28, and click Save. You should see something that looks a lot like Figure 5-29. Click Done, and take the table view out of edit mode. Clicking on the new city will take you to the city page; apart from the blank space where the picture will be placed it should look the same as the other city pages in the guide.

If you don’t enter a city name in the Add City view, or if you click on the Back button on the left rather than the Save button, no changes will be made to either the cities array or the data model held by the application delegate.

The blank space where the image should be on our newly added city is a bit annoying; the easiest way to get around this is to add a default image. The image you choose to use for this placeholder image isn’t really relevant. I used the classic image of a question mark on top of a folder, the image Mac OS X would display if it could not find my boot disk, but you can use anything. Remember to keep your aspect ratio the same as you scale your image, and copy it into your project, as we did with the other city images.

The Add New City view

Figure 5-28. The Add New City view

The City Guide view in edit mode with our new city added to the list

Figure 5-29. The City Guide view in edit mode with our new city added to the list

You can add the image to the viewDidLoad: method of the CityController class. You’ll be replacing the last line of code in the method (pictureView.image = thisCity.cityPicture;) with the code shown in bold:

- (void)viewDidLoad {

CityGuideDelegate *delegate = (CityGuideDelegate *)

[[UIApplication sharedApplication] delegate];

City *thisCity = [delegate.cities objectAtIndex:index.row];

self.title = thisCity.cityName;

descriptionView.text = thisCity.cityDescription;

descriptionView.editable = NO;

UIImage *image = thisCity.cityPicture;

if ( image == nil ) {

image =[UIImage imageNamed:@"QuestionMark.jpg"];

}

pictureView.image = image;

}

Here we added a check to see whether the cityPicture returned by the City object is equal to nil. If so, we simply substitute the default image; this should produce something similar to Figure 5-30.

The default image displayed in the CityController view

Figure 5-30. The default image displayed in the CityController view

We’re done, at least for this chapter. We’ll come back to the City Guide application to fix the remaining problems later. For instance, we’ll return to it briefly in the next chapter where I’ll show you how to use the UIImagePickerController to attach images to your new City Guide entries. We’ll also come back to it again in Chapter 8 where I’ll address how to store your data. At the moment, while users can add new cities and delete cities, if they quit the application and restart it they’ll be back to the original default set of cities.