10.3. Section Lists

The UITableView class is very versatile. It not only accommodates standard list-like tables and preferences tables, but also another type of table commonly used in iPhone software: section lists. When a table gets long enough, finding an item can be like finding a needle in a haystack. A section list provides a visual structure similar to a standard table, but expands it to include individual row groupings and a Rolodex-like index to quickly flip to a section heading. You can assign each grouping a section title, such as genres in a book or the first letter of a contact. You'll find section lists in use in the iPhone's own contact and song lists.

Like other tables, the section list uses a data source. A data source is a protocol interface to query an object for the contents and construction of the table. The data source for a section list provides the protocol methods needed to build the section list's groupings, section titles, index, and individual row cells. As with other table classes covered in this book, the examples provided here create a subclass of the UITableViewController object that can both contain a section table and the accompanying data source. This architecture is the easiest to illustrate and is ideal for creating specialized reusable classes.

NOTE

 

In a lower level of the framework, UI Kit instantiates a class named UISectionList, which encapsulates a UISectionTable, comprising the table portion of the list. As is the case with preferences tables, these lower-level objects are hidden from the developer, so you'll once again be working with the UITableView class's standard interfaces to build this object.

10.3.1. Creating the Section List

You create section lists in much the same way as a preferences table, and use the same overrides to base their construction on. The two differences between the construction of a section list and a preferences table are the underlying table structure and extra delegate methods to add index sidebar.

The two table formats are so similar, in fact, that simply replacing the following line from the ShootStuffUp example in the last section will render your preferences table as a section list.

Replace the following code:

self = [ super initWithStyle: UITableViewStyleGrouped ];

With this code:

self = [ super init ];

When you create the underlying table, its default style now allows for sections and indexing (Figure 10-3). By replacing the preceding line of code with a call to init, you're returning the table style to its default, which is created internally when not done manually:

Figure 10-3. ShootStuffUp as a section list


Your table view controller will use some of the same delegate methods as a preferences table. An example follows:

#import <UIKit/UIKit.h>

@interface MySectionListViewController : UITableViewController {

}

- (id) init;
- (void) dealloc;
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView;
- (NSInteger)tableView:(UITableView *)tableView
    numberOfRowsInSection:(NSInteger)section;
- (NSString *)tableView:(UITableView *)tableView
   titleForHeaderInSection:(NSInteger)section;
- (UITableViewCell *)tableView:(UITableView *)tableView
   cellForRowAtIndexPath:(NSIndexPath *)indexPath;

@end

If you will be adding an index bar (the Rolodex-style bar that appears in your iPhone's contacts list), you'll add one additional override to obtain the index names:

- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView;

The methods used for the data source break down as follows:



numberOfSectionsInTableView

Returns the number of individual sections to be displayed in the preferences table. Only include empty sections if you want them displayed as empty.



numberOfRowsInSection

Returns the number of rows in a given section.



cellForRowAtIndexPath

Returns a UITableViewCell object corresponding to the section and row specified. This method should check for an existing cell on the queue in the same way as the TableDemo table example from Chapter 3.



titleForHeaderInSection

Returns an NSString object containing the section label for the given section. This can be a single character (such as in contact lists) or full strings (such as genres of books).



heightForRowAtIndexPath

Returns a custom height for the group and row specified. This allows you to customize certain cells to be a specific height.



sectionIndexTitlesForTableView

Returns an NSArray object containing an array of NSString objects to use to build an index bar.



sectionForSectionIndexTitle

Associates a given index title with a section number so that tapping on the index will position the section on-screen. This is especially useful when creating nonalphabetic indexes, such as book genres, etc.

10.3.2. Adding an Index Bar

When you add an index bar to a section list, a Rolodex-style bar appears to the right, allowing the user to quickly select the desired section by clicking on an index tab. Index titles are traditionally alphabetic, but you may configure an index bar to have any given set of titles.

To add an index bar, add a data source method named sectionindexTitlesForTableView. An example follows:

- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView {

    return [ NSMutableArray arrayWithObjects:
         @"A", @"B", @"C", @"D", @"E", @"F",
         @"G", @"H", @"I", @"J", @"K", @"L",
         @"M", @"N", @"O", @"P", @"Q", @"R",
         @"S", @"T", @"U", @"V", @"W", @"X",
         @"Y", @"Z", @"#", nil
    ];
}

10.3.3. Displaying the Section List

A section list is displayed in the same way as a standard table view controller—by attaching its underlying view to a window or by pushing it onto a navigation controller.

Use the following to set the active view for the window:

[ window addSubview: myTableViewController.view ];

Use the following to push it onto a navigation controller:

[ navigationController pushViewController: myTableViewController.view
        animated: YES ]
    ];

10.3.4. TableDemo: A Better File Browser

The TableDemo example from Chapter 3 displays a simple table containing a list of files and directories present in your application's home directory on the iPhone. This version of the table demo creates a separate alphabetical section for files and places each file in the section corresponding to its first letter (Figure 10-4). The example continues to makes use of a table view controller, and attaches this to a navigation bar controller to present user editing buttons and a Reload button. The user will be able to press Edit 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 10-4. TableDemo example


You can compile this application, shown in Examples Example 10-6 through Example 10-10, 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 10-6. 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 10-7. 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 ]
        initWithRootViewController: viewController ];

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

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

@end

                                          

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

@interface TableDemoViewController : UITableViewController {

    int nActiveSections;
    NSMutableArray *fileList[27];
    NSMutableArray *activeSections;
    NSMutableArray *sectionTitles;
}

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

@end

Example 10-9. 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;
}

- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView {

    return [ NSMutableArray arrayWithObjects:
         @"A", @"B", @"C", @"D", @"E", @"F",
         @"G", @"H", @"I", @"J", @"K", @"L",
         @"M", @"N", @"O", @"P", @"Q", @"R",
         @"S", @"T", @"U", @"V", @"W", @"X",
         @"Y", @"Z", @"#", nil
    ];
}

- (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;

    for(int i=0;i<27;i++) {
        fileList[i] = [ [ NSMutableArray alloc ] init ];
    }
    dirEnum = [ [ NSFileManager defaultManager ] enumeratorAtPath:
        NSHomeDirectory()
    ];

    while ((file = [ dirEnum nextObject ])) {
        char index = ( [ file cStringUsingEncoding: NSASCIIStringEncoding ] )[0];

        if (index >= 'a' && index <= 'z') {
            index -= 32;
        }
        if (index >= 'A' && index <= 'Z') {
            index -= 65;
            [ fileList[(int) index] addObject: file ];
        } else {
            [ fileList[26] addObject: file ];
        }
    }

    nActiveSections = 0;
    activeSections = [ [ NSMutableArray alloc ] init ];
    sectionTitles = [ [ NSMutableArray alloc ] init ];
    for(int i=0;i<27;i++) {
        if ( [fileList[i] count ]>0) {
            nActiveSections++;
            [ activeSections addObject: fileList[i] ];
            if (i < 26)
                [ sectionTitles addObject: [ NSString stringWithFormat:
                       @"%c", i + 65 ] ];
            else
                [ sectionTitles addObject: @"0-9" ];
        }
    }

    [ self.tableView reloadData ];
}

- (NSString *)tableView:(UITableView *)tableView
    titleForHeaderInSection:(NSInteger)section
{
    return [ sectionTitles objectAtIndex: section ];
}

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


- (NSInteger)tableView:(UITableView *)tableView
    numberOfRowsInSection:(NSInteger)section
{
    return [ [ activeSections objectAtIndex: section] count ];
}

- (NSInteger)tableView:(UITableView *)tableView sectionForSection
IndexTitle:(NSString *)title atIndex:(NSInteger) index {
    int i = 0;
    for (NSString * sectionTitle in sectionTitles) {
        if ([ sectionTitle isEqualToString: title ]) {
            [ tableView scrollToRowAtIndexPath:
                [ NSIndexPath indexPathForRow: 0 inSection: i ]
                   atScrollPosition: UITableViewScrollPositionTop animated: YES ];
            return i;
        }
        i++;
    }
    return -1;
}

- (UITableViewCell *)tableView:(UITableView *)tableView
    cellForRowAtIndexPath:(NSIndexPath *)indexPath
{

    NSString *CellIdentifier = [[ activeSections objectAtIndex:
        [ indexPath indexAtPosition: 0 ]] 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 < [ [ activeSections objectAtIndex:
            [ indexPath indexAtPosition: 0 ] ] count ]; i++)
        {
            if ([ cell.text isEqualToString: [
                [ activeSections objectAtIndex: [ indexPath indexAtPosition: 0 ] ]
                objectAtIndex: i ] ])
            {
                [ [ activeSections objectAtIndex:
                    [ indexPath indexAtPosition: 0 ] ] removeObjectAtIndex: i ];
            }
        }

        /* Delete cell from table */

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

- (void)tableView:(UITableView *)tableView
    didSelectRowAtIndexPath:(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:
    (UIInterfaceOrientation)interfaceOrientation
{
    return YES;
}


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


- (void)dealloc {
    for(int i=0;i<27;i++) {
        [ fileList[i] release ];
    }
    [ super dealloc ];
}

@end

                                          

Example 10-10. 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;
}

                                          

10.3.5. What's Going On

The TableDemo example works as follows:

  1. The application instantiates as all other examples thus far have; by calling the program's main function, 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 of NSMutableArray objects named fileList based on its first character. If the first character is alphabetical, it's added in fileList arrays 0-25, otherwise it's added to fileList array 26.

  3. All 27 fileList arrays are checked for contents. Nonempty arrays are added as sections to the section table by adding them to an array named activeSections. Array titles are also stored separately so we can keep track of which sections are being displayed.

  4. When the table is rendered, the controller's data source methods are automatically called. The numberOfRowsInSection method returns the number of rows for the section, whose contents are stored in activeSections. The cellForRowAtIndexPath creates a new cell using the filename as the cell title.

  5. 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.

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

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

10.3.6. 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. Create a new section for each directory and add the contents of each directory to the section. Use the directory name for the section name.

  • 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.

  • If you haven't already done so, check out the following prototypes in your SDK's header files: UITableView.h, UITableViewCell.h, and UITableViewController.h. You'll find these deep within /Developer/Platforms/iPhoneOS.platform, inside the UI Kit framework's Headers directory.