3.8. Table Views and Controllers

Tables are the foundation for most types of selectable lists on the iPhone. Voicemail, recent calls, and even email all use the feature-rich UITableView class to display their lists of items. In addition to being a basic list selector, the UITableView class includes built-in functionality to handle swipe-to-delete, add disclosure accessories, animations, labels, and even images. A special subclass of view controller exists specifically for tables. The UITableViewController class encapsulates a single UITableView object and provides the plumbing to handle your business logic, screen rotations, navigation bar properties, and all of the other benefits that come with using view controllers.

3.8.1. Creating the Table

A table has three primary components: the table itself, table sections (or groupings), and table cells (the individual rows in a table). The table's data is queried from a table's data source. A data source is an object that provides information to the table about which data to display, such as filenames, email messages, etc. The data source must implement the UITableViewDataSource protocol, and respond to a specific subset of methods for providing this information to the table.

When you create the table, you'll provide a pointer to the object that will act as a data source. The data source will be called whenever the table is reloaded or new cells are scrolled into view, so that the table can receive instruction about which sections and rows to display, and provide the data for each.

3.8.1.1. Subclassing UITableViewController

For most specialized uses, the table view controller can serve as the data source for its underlying table. This allows the table class and the table's data to be wrapped cleanly into a single controller class. In fact, the UITableViewController class is already designed to implement the UITableViewDataSource protocol.

Create a subclass of the UITableViewController object. In the following example, a subclass named MyTableViewController is created. The base class methods used to initialize and destroy the object are overridden to integrate the table portion of the class:

@interface MyTableViewController : UITableViewController
{

}
-(id)init;
-(void)dealloc;

To add the data source portion of the class, you need to write three methods that answer the data binding's request for data: numberOfSectionsInTableView, numberOfRowsInSec⁠tion and cellForRowAtIndexPath. Because the table controller is acting as the data source, you'll write these methods into your controller's subclass.

Use the numberOfSectionsInTableView method to create a set of unique groups within the table. These are most commonly found when creating grouped preferences tables or section lists. You'll learn more about these in Chapter 10. A standard table consists of only one section, so hardcode this for now:

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

The numberOfRowsInSection method should return the number of rows in each section of the table. Since a standard table only includes one section, the value returned should equal the total number of rows in the table. You'll tie this value in with your actual data:

- (NSInteger)tableView:(UITableView *)tableView numberOf
RowsInSection:(NSInteger)section
{
    return nRecords;
}

Finally, the method named cellForRowAtIndexPath returns a UITableViewCell object containing the display information for the given table cell. The UITableViewCell class is a versatile class supporting text and images, and includes editing and delete confirmation functionality.

Each cell is cached in memory when it is created, so if it's been created before, you'll be able to find it in the table's queue. This allows the table to scroll without needing to recreate the same cells as they fall in and out of view. It also allows a table to internally jettison unused cells when memory is low, recreating them later if necessary. For this reason, your data binding should be prepared to service the same cell multiple times:

- (UITableViewCell *)tableView:(UITableView *)tableView
    cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSString *CellIdentifier = [ [ NSString alloc ] initWithFormat:
        @"Cell %d", [ indexPath indexAtPosition: 1 ] ];

    /* Look up cell in the table queue */
    UITableViewCell *cell =
        [ tableView dequeueReusableCellWithIdentifier: CellIdentifier ];

    /* Not found in queue, create a new cell object */
    if (cell == nil) {
        cell = [ [ [ UITableViewCell alloc ]
            initWithFrame: CGRectZero reuseIdentifier: CellIdentifier ]
        autorelease ];
    }

    /* Set some text for the cell */
    cell.text = CellIdentifier;
    return cell;
}

The index path used in this example refers to a foundation object named NSIndexPath. This object allows not just one index, but several, to be specified within a single object. When referencing the index within a table, the NSIndexPath object contains a section number and a cell number. You can query each position of the index using the indexAtPosition method. The first position (position 0) always references the section number of the cell, and the second position (position 1) always references the row number of the cell within the given section. For simple tables, you'll only have a single section, so the section number will always resolve to 0.

You'll look at these methods in more detail later in this chapter.

3.8.2. Table Cells

A table references every record as a table cell object. Instead of thinking as a table cell as just text, think of a table cell as a miniature canvas. The UITableViewCell class provides the functionality to tailor a table cell to accommodate a custom look and feel. Cells can include images, text, labels, and a variety of styles. As you've seen, table cells are queued by the table, and so you'll only need to create cells the first time they're used, or again only if they've been purged from memory.

Each cell is assigned a reuse identifier when created. This is used to identify the cell within the table's queue. In the previous example, you incorporated the cell number into the identifier, but you can set any unique value you like:

NSString *CellIdentifier = [ [ NSString alloc ] initWithString: @"Frank" ];
UITableViewCell *cell = [ [ [ UITableViewCell alloc ]
            initWithFrame: CGRectZero
            reuseIdentifier: CellIdentifier
    ] autorelease
];

After creating a table cell, you can assign a number of different styling options to it.

3.8.2.1. Display text

To add display text to the cell, use the cell's text property:

cell.text = @"Frank's Table Cell";

3.8.2.2. Alignment

Adjust the cell's text alignment by setting the textAlignment property. You used similar properties when working with text views:

cell.textAlignment = UITextAlignmentLeft;

The default text alignment is left-aligned, but you may use any of the following values. These are the same values you learned about earlier, used with the UITextView class:



UITextAlignmentLeft

Text will be left-aligned (default)



UITextAlignmentRight

Text will be right-aligned



UITextAlignmentCenter

Text will be centered

3.8.2.3. Font and size

To set the text font and point size for the cell, assign a UIFont object to the text view's font property. This functions in the same way as setting a UITextView object's font. To create a UIFont object, import UI Kit's UIFont.h header:

#import <UIKit/UIFont.h>

You can use a static method named fontWithName to easily instantiate new fonts:

UIFont *myFont = [ UIFont fontWithName: @"Arial" size: 18.0 ];
cell.font = myFont;

Additionally, three other static methods exist for easily creating system fonts:

UIFont *mySystemFont = [ UIFont systemFontOfSize: 12.0 ];
UIFont *myBoldSystemFont = [ UIFont boldSystemFontOfSize: 12.0 ];
UIFont *myItalicSystemFont = [ UIFont italicSystemFontOfSize: 12.0 ];

Font selection determines the display font for all text within the cell only. A table cell does not directly support rich text.

3.8.2.4. Text color

You can define the cell's text color by assigning a UIColor object to the cell's textColor property. To create a UIColor object, import UI Kit's UIColor.h header:

#import <UIKit/UIColor.h>

You can use static methods to create color objects, which are autoreleased when no longer needed. Colors can be created as white levels, using hue, or as an RGB composite. You've already learned about colors earlier in this chapter.

Once you have created a UIColor object, assign it to the cell's textColor property:

cell.textColor = [ UIColor redColor ];

You can also set the color of the text for a highlighted (selected) cell, using the selectedTextColor property:

cell.selectedTextColor = [ UIColor blueColor ];

Because a cell doesn't directly support rich text, the color selection affects all of the text within the cell.

3.8.2.5. Images

To add an image to a cell, assign a UIImage object to the cell's image property:

cell.image = [ UIImage imageNamed: @"cell.png" ];

You can also set the image to be displayed when the cell is in its selected state:

cell.selectedImage = [ UIImage imageNamed: @"selected_cell.png" ];

The UIImage class's imageNamed method will retrieve an image from your application folder. All of the images used in your table should have roughly the same dimensions so that your table looks uniform. Depending on the height of your image, you may need to adjust the row height of the table. You can set a standard row height for the entire table by overriding the table viewcontroller's init method to set the table's rowHeight property:

- (id)init {
    self = [ super init ];
    if (self != nil) {
        self.tableView.rowHeight = 65;
    }

    return self;
}

Alternatively, if you wish to have variable row height cells, you can define a different height for each cell individually by overriding the heightForRowAtIndexPath method within the data source:

- (CGFloat)tableView:(UITableView *)tableView
    heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    if ([ indexPath indexAtPosition: 1 ] == 0)
        return 65.0;
    else
        return 40.0;
}

3.8.2.6. Selection style

By changing the cell's selectionStyle property, you can customize the color used when a cell is highlighted:

cell.selectionStyle = UITableViewCellSelectionStyleBlue;

The default is to highlight the cell in blue; however, the following options are available:



UITableViewCellSelectionStyleBlue

Highlight selected cells in blue



UITableViewCellSelectionStyleGray

Highlight selected cells in gray



UITableViewCellSelectionStyleNone

Do not highlight selected cells

3.8.2.7. Labels

Labels are miniature view classes that you can add to table cells to further augment the cell with decorated text. Labels are used to add peripheral text to a cell, such as the day, subject, and preview displayed in Apple's Mail application.

A label is initialized with a display region offset to the cell. The following example creates a label that is offset 100x0 within the table cell, and is 50x50 in size:

UILabel *label = [ [ UILabel alloc ] initWithFrame:
    CGRectMake(100.0, 0.0, 50.0, 50.0)
];

Set the label text using the label's text property:

label.text = @"Label Text";

A label shares many of the same text properties as a cell or a text view. These include text alignment, color, and font:

label.textAlignment = UITextAlignmentLeft;
label.textColor = [ UIColor redColor ];
label.font = [ UIFont fontWithName: @"Arial" size: 10.0 ];

A label also allows you to use text shadowing. You can even define an offset for the shadow by passing a CGSize structure to the label's shadowOffset property:

label.shadowColor = [ UIColor grayColor ];
label.shadowOffset = CGSizeMake(0, -1);

Labels also allow you to define the text color to be used when the cell is highlighted:

label.highlightedTextColor = [ UIColor blackColor ];

If you really want to uglify your label, you can even set a background color. The background color will only be displayed within the label's display region. This can be useful for debugging when manually laying out labels, or to scare your customers away:

label.backgroundColor = [ UIColor blueColor ];

Once you have created the label, attach it to the cell as a subview:

[ cell addSubview: label ];

3.8.2.8. Disclosures

Disclosures, also known as accessories, are icons appearing at the right side of a table cell to disclose that there is another level of information to be displayed when the cell is selected. These are commonly used on desktop interfaces such as iTunes, where the user first selects a genre, then artist, and finally a song.

In addition to arrows, a checkbox can be displayed to indicate cells that have been selected in tables allowing multiple selections.

Any given cell can display an accessory by setting the cell's accessoryType property:

cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;

The following accessory styles are available.

Style Description
UITableViewCellAccessoryNone No accessory
UITableViewCellAccessoryDisclosureIndicator Black right-arrow chevron
UITableViewCellAccessoryDetailDisclosureButton Blue disclosure button
UITableViewCellAccessoryCheckmark Checkmark, for selection

3.8.3. Implementing Multiple Select

As you've learned, a table cell can display a checkbox accessory via the accessoryType property. When the user selects a cell, the table delegate's didSelectRowAtIndexPath method is invoked. This method is part of the UITableViewDelegate protocol. Adding this method to your delegate will allow you to add multiple selection support to your table by controlling which cells are set with a checkbox accessory:

- (void)tableView:(UITableView *)tableView
    didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSLog(@"Selected section %d, cell %d",
        [ indexPath indexAtPosition: 0 ], [ indexPath indexAtPosition: 1 ]);

    /* Get a pointer to the selected table cell */
    UITableViewCell *cell = [ self.tableView cellForRowAtIndexPath: indexPath ];

    /* Toggle the accessory type */
    if (cell.accessoryType == UITableViewCellAccessoryNone)
        cell.accessoryType = UITableViewCellAccessoryCheckmark;
    else
        cell.accessoryType = UITableViewCellAccessoryNone;
}

                                          

3.8.4. Editing and Swipe-to-Delete

To allow the user to delete objects in a table, enable the table's editing feature. This will cause each cell in the table to display a red deletion icon to the left. Each table cell will be indented automatically during editing:

[ self.tableView setEditing:YES animated:YES ];

This action should be triggered by a navigation bar button action, such as a button labeled Edit, or by some similar action. Use the same method to allow the user to leave editing mode when finished editing:

[ self.tableView setEditing: NO animated: YES ];

During the editing process, a user may delete a record from the table. The user will be presented with a delete confirmation. After confirming, the data source's commitEditingStyle method is notified to inform your application that a delete was requested. This too is a method belonging to the UITableViewDataSource protocol. It's up to you to service this request by deleting the underlying data from your data source. You'll also instruct the table view to delete the row:

- (void)tableView:(UITableView *)tableView
    commitEditingStyle:(UITableViewCellEditingStyle) editingStyle
    forRowAtIndexPath:(NSIndexPath *) indexPath
{
    if (editingStyle == UITableViewCellEditingStyleDelete) {
        NSLog(@"Deleted section %d, cell %d",
            [ indexPath indexAtPosition: 0 ], [ indexPath indexAtPosition: 1 ]);

        /* Additional code to delete the cell from your data */

        /* Delete cell from the table */

        NSMutableArray *array = [ [ NSMutableArray alloc ] init ];
        [ array addObject: indexPath ];
        [ self.tableView deleteRowsAtIndexPaths: array
            withRowAnimation: UITableViewRowAnimationFade
        ];
    }
}

                                          

NOTE

 

If this method exists in your delegate class, swipe-to-delete functionality will automatically be made active.

The deleteRowsAtIndexPaths method allows you to delete one or more rows from a table by passing an array of index paths. You can also specify one of a few predefined animations you'd like to use to delete the cells. The following animations are supported.

Animation Description
UITableViewRowAnimationFade Cell fades out
UITableViewRowAnimationRight Cell slides out from right
UITableViewRowAnimationLeft Cell slides out from left
UITableViewRowAnimationTop Cell slides out to top of adjacent cell
UITableViewRowAnimationBottom Cell slides out to bottom of adjacent cell

3.8.5. Reloading Tables

If the data within your table has changed, reload the table by invoking the table view's reloadData method. The data source will be queried again, and whatever changes you've made to your underlying data will propagate to the table:

[ self.tableView reloadData ];

This is only preferred if the table data has changed since the user last interacted with it, and is not recommended for reloading the table after a cell has been deleted. When the table is reloaded, the entire table structure is broken down and rebuilt, so you'll want to be conservative in its use. For the deletion of single cells, use the dele⁠teR⁠owsA⁠tIn⁠dexPaths method to animate the deletion of one or more cells.

3.8.6. TableDemo: Simple File Browser

The TableDemo example displays a table containing a list of files and directories present in your application's home directory on the iPhone, as shown in Figure 3-4. The example makes use of a table view controller, attached to a navigation bar controller to present user editing buttons and a reload button. The user will be able to press the Edit button to delete items from the list (don't worry, it won't actually delete any files). Swipe functionality is also active, allowing the user to swipe to delete a cell.

Figure 3-4. TableDemo example


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

Example 3-32. TableDemo application delegate prototypes (TableDemoAppDelegate.h)
#import <UIKit/UIKit.h>

@class TableDemoViewController;

@interface TableDemoAppDelegate : NSObject <UIApplicationDelegate> {
    UIWindow *window;
    TableDemoViewController *viewController;
    UINavigationController *navigationController;
}

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

@end

                                          

Example 3-33. TableDemo application delegate (TableDemoAppDelegate.m)
#import "TableDemoAppDelegate.h"
#import "TableDemoViewController.h"

@implementation TableDemoAppDelegate

@synthesize window;
@synthesize viewController;


- (void)applicationDidFinishLaunching:(UIApplication *)application {
    CGRect screenBounds = [ [ UIScreen mainScreen ] bounds ];

    self.window = [ [ [ UIWindow alloc ] initWithFrame: screenBounds ] autorelease ];
    viewController = [ [ TableDemoViewController alloc ] init ];
    navigationController = [ [ UINavigationController alloc ] init
WithRootViewController: viewController ];

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


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

@end

                                          

Example 3-34. TableDemo view controller prototype (TableDemoViewController.h)
#import <UIKit/UIKit.h>

@interface TableDemoViewController : UITableViewController {
    NSMutableArray *fileList;
}

- (void) startEditing;
- (void) stopEditing;
- (void) reload;

@end

Example 3-35. TableDemo view controller (TableDemoViewController.m)
#import "TableDemoViewController.h"

@implementation TableDemoViewController

- (id)init {
    self = [ super init ];

    if (self != nil) {

        /* Build a list of files */
        [ self reload ];

        /* Initialize navigation bar buttons */

        self.navigationItem.rightBarButtonItem
        = [ [ [ UIBarButtonItem alloc ]
           initWithBarButtonSystemItem: UIBarButtonSystemItemEdit
           target: self
           action: @selector(startEditing) ] autorelease ];

        self.navigationItem.leftBarButtonItem
        = [ [ [ UIBarButtonItem alloc ]
             initWithTitle:@"Reload"
             style: UIBarButtonItemStylePlain
             target: self
             action:@selector(reload) ]
           autorelease ];
    }

    return self;
}

- (void) startEditing {
    [ self.tableView setEditing: YES animated: YES ];

    self.navigationItem.rightBarButtonItem
    = [ [ [ UIBarButtonItem alloc ]
       initWithBarButtonSystemItem: UIBarButtonSystemItemDone
       target: self
       action: @selector(stopEditing) ] autorelease ];
}

- (void) stopEditing {
    [ self.tableView setEditing: NO animated: YES ];

    self.navigationItem.rightBarButtonItem
    = [ [ [ UIBarButtonItem alloc ]
       initWithBarButtonSystemItem: UIBarButtonSystemItemEdit
       target: self
       action: @selector(startEditing) ] autorelease ];
}

- (void) reload {
    NSDirectoryEnumerator *dirEnum;
    NSString *file;

    fileList = [ [ NSMutableArray alloc ] init ];
    dirEnum = [ [ NSFileManager defaultManager ] enumeratorAtPath:
        NSHomeDirectory()
    ];

    while ((file = [ dirEnum nextObject ])) {
        [ fileList addObject: file ];
    }

    [ self.tableView reloadData ];
}

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


- (NSInteger)tableView:(UITableView *)tableView numberOfRowsIn
Section:(NSInteger)section {

    return [ fileList count ];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRow
AtIndexPath:(NSIndexPath *)indexPath {

    NSString *CellIdentifier = [ fileList objectAtIndex:
        [ indexPath indexAtPosition: 1 ]
    ];

    UITableViewCell *cell = [ tableView
        dequeueReusableCellWithIdentifier: CellIdentifier
    ];

    if (cell == nil) {
        cell = [ [ [ UITableViewCell alloc ] initWithFrame:
            CGRectZero reuseIdentifier: CellIdentifier ] autorelease
        ];
        cell.text = CellIdentifier;

        UIFont *font = [ UIFont fontWithName: @"Courier" size: 12.0 ];
        cell.font = font;
    }

    return cell;
}

- (void)tableView:(UITableView *)tableView
    commitEditingStyle:(UITableViewCellEditingStyle) editingStyle
    forRowAtIndexPath:(NSIndexPath *) indexPath
{
    if (editingStyle == UITableViewCellEditingStyleDelete) {

        /* Delete cell from data source */

        UITableViewCell *cell = [ self.tableView cellForRowAtIndexPath:
            indexPath ];

        for(int i = 0; i < [ fileList count ]; i++) {
            if ([ cell.text isEqualToString: [ fileList objectAtIndex: i ] ]) {
                [ fileList removeObjectAtIndex: i ];
            }
        }

        /* Delete cell from table */

        NSMutableArray *array = [ [ NSMutableArray alloc ] init ];
        [ array addObject: indexPath ];
        [ self.tableView deleteRowsAtIndexPaths: array
            withRowAnimation: UITableViewRowAnimationTop
        ];

    }
}

- (void)tableView:(UITableView *)tableView didSelectRowAt
IndexPath:(NSIndexPath *)indexPath {

    UITableViewCell *cell = [ self.tableView cellForRowAtIndexPath: indexPath ];

    UIAlertView *alert = [ [ UIAlertView alloc ] initWithTitle: @"File Selected"
        message: [ NSString stringWithFormat: @"You selected the file '%@'",
                       cell.text ]
        delegate: nil
        cancelButtonTitle: nil
        otherButtonTitles: @"OK", nil
    ];

    [ alert show ];
}

- (void)loadView {
    [ super loadView ];
}

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterface
Orientation)interfaceOrientation {
    return YES;
}


- (void)didReceiveMemoryWarning {
    [ super didReceiveMemoryWarning ];

}

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

@end

                                          

Example 3-36. TableDemo main (main.m)
#import <UIKit/UIKit.h>

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

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

                                          

3.8.7. What's Going On

The TableDemo example works as follows:

  1. The application instantiates as all other examples thus far have, and the program's main function is called, which invokes the TableDemoAppDelegate class's applicationDidFinishLaunching method, and builds the appropriate window, view controller, and navigation controller classes.

  2. The view controller is created as an instance of UITableViewController. The controller's init method is overridden to load a list of files into an array named fileList. It also adds two navigation bar buttons: a system Edit button and a Reload button.

  3. When the table is rendered, the controller's data source methods are automatically queried. The numberOfRowsInSection method returns the number of rows in the file list. The cellForRowAtIndexPath creates a new cell using the filename as the cell title.

  4. If the user taps the Edit button, the button's designated selector, startEditing, is notified. This replaces the button with a Done button and enables editing of the table. The table automatically indents each cell and adds a delete icon.

  5. If the user deletes a row either by editing or swiping, he will be asked for confirmation. Once the user confirms, the delete request will cause the commitEditingStyle method to be notified of the request. The method checks to ensure the request is a delete request and deletes the object from the file list and the table.

  6. If the user presses the Reload button, the file list is reread and the table is refreshed.

3.8.8. Further Study

See if you can take what you've previously learned and apply it to this example:

  • Use NSDirectoryEnumerator class's fileAttributes method to identify which files are directories. Add a disclosure arrow for cells that reference directories, and push a new table view controller on the stack when the user taps one. The child view controller should have a Back button and display only files that are present in the selected directory.

  • Add a new view controller that displays the contents of a file in a UITextView. If the file is binary, display a hexadecimal readout. When the user taps on a file, a new view controller should be pushed on the navigation controller's stack that displays the contents of the file. The new view controller should have a Back button.

  • Check out the following prototypes in your SDK's header files: UITableView.h, UITableViewCell.h, UILabel.h, and UITableViewController.h. You'll find these under /Developer/Platforms/iPhoneOS.platform, inside the UI Kit framework's Headers directory.