A technique for using UITableView and retaining your sanity

I love programming UIKit. It's really slick and, although it's lacking a few things that I would really like to have, it's already very powerful. In UIKit programming, the table is fundamental. Almost anything you see that appears in rows on the iPhone is data within a UITableView object.

The iPhone does not support Cocoa Bindings, Apple's technology for automatically synchronising the model and view layers of your application. This means we're back to the Jaguar days in which the developer has to specify, in code, the values to be shown in every row and column of the table. UITableView only supports one column, so the problem is reduced, but the fact remains that one has to specify the data in code.

Owing to the fact that UITableView is so fundamental to UIKit, it supports a lot of functionality. UITableView supports sections, and an individual row is specified with an NSIndexPath object which can be queried for its section and row properties. The row value only makes sense when interpreted in the light of the section value (i.e. there are many rows at index zero, but only one at section zero, index zero).

I want to discuss a sanity-saving technique for implementing some of the core methods that must be implemented to create a grouped UITableView (the style with rounded rows and a blue pinstripe background). There are many, many other delegate and data source methods that you may need to implement for your particular application, but I'll just show the methods that everyone will deal with. The technique is applicable anywhere that you need to identify rows and sections of a UITableView from an NSIndexPath object.

The fundamental thing I want to get across to you is this: if you are comparing elements of NSIndexPath to integer literals in your code, you're doing it wrong.

The reason you're doing it wrong is because of what happens when you want to reorder a few rows in a section, or reorder the sections of your table. There are too many places you need to remember to change, so your code is brittle. UITableViewDataSource defines ten methods in which you will be passed integers or NSIndexPath objects identifying table rows or sections. UITableViewDelegate defines a further twenty. It's unlikely that any application will need to implement all thirty data source and delegate methods, but I suggest that implementing six to ten is not unlikely.

What I suggest you do is use C enums to define symbols for both the sections and rows within each section. Take this hypothetical example about books:

enum Sections {
kHeaderSection = 0,
kTitleSection,
kAuthorSection,
kBodySection,
NUM_SECTIONS
};

enum HeaderSectionRows {
kHeaderSectionCopyrightRow = 0,
kHeaderSectionPublisherRow,
NUM_HEADER_SECTION_ROWS
};


What I have shown here are symbols for each section index and each row in the header section. One would obviously define enums for each row in each section. I'll explain the NUM_SECTIONS and NUM_HEADER_SECTION_ROWS entries in a moment.

Now, when it comes to implementing the core parts of the UITableViewDataSource and UITableViewDelegate protocols, we never compare the given NSIndexPath values to integer literals. Here are the two core methods dealing with the size of the table and of each section:

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

- (NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section
{
switch(section) {
case kHeaderSection:
return NUM_HEADER_SECTION_ROWS;
default:
return 1;
}
}


The NUM_SECTIONS and NUM_HEADER_SECTION_ROWS values are not 'true' values in their respective enums. They don't refer to any index used in the table, but they do provide a symbol for the number of 'real' values in each enum. This only works if the enums are zero-based, as they usually are in UITableView programming. This ensures that we don't have to change -numberOfSectionsInTableView: or -tableView:numberOfRowsInSection: when we add a section to the table, or a row to any section. Shared credit to Jim Correia, Andy Finnell and Jose Vazquez for independently clueing me in to this technique via Twitter.

The next 'core' method that you need to implement deals with returning an appropriate instance of UITableViewCell for each row in each section of the table. Again, we want this method to be robust in the face of adding to or reordering rows and sections of the table. Here's an implementation using enums:


- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
if(indexPath.section == kHeaderSection) {
switch(indexPath.row) {
case kHeaderSectionCopyrightRow:
// Assume these cells are already configured somehow.
// Omitting the configuration for brevity.
return copyrightCell;
case kHeaderSectionPublisherRow:
return publisherCell;
default:
NSAssert(NO, @"Unhandled value in kHeaderSection cellForRowAtIndexPath");
}
}

if(indexPath.section == kTitleSection) {
switch(indexPath.row) {
case kTitleSectionTitleRow:
return titleCell;
// ...etc..
default:
NSAssert(NO, @"Unhandled value in kTitleSection cellForRowAtIndexPath");
}
}
}


Now, no matter whether the header section is section 0, 1 or 2, and no matter whether the copyright or publisher cells come first in the header section, this method will return the correct cells. Not only that, but reordering the sections or the rows within each section simply requires reordering the values in the enum. You don't have to touch the code.

Compare the above implementations to the same code, implemented with integer literals:

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

- (NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section
{
switch(section) {
case 0:
return 2;
default:
return 1;
}
}

- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
if(indexPath.section == 0) {
switch(indexPath.row) {
case 0:
// Assume these cells are already configured somehow.
// Omitting the configuration for brevity.
return copyrightCell;
case 1:
return publisherCell;
default:
NSAssert(NO, @"Unhandled value in kHeaderSection cellForRowAtIndexPath");
}
}

if(indexPath.section == 1) {
switch(indexPath.row) {
case 0:
return titleCell;
// ...etc..
default:
NSAssert(NO, @"Unhandled value in kTitleSection cellForRowAtIndexPath");
}
}
}


Suddenly this code is a lot less informative and far more fragile. Want to swap the header and title sections? Well you had better remember to switch the returned value in case 0 of -tableView:numberOfRowsInSection:, and you had better remember to swap the code inside if(indexPath.section == 0) with the code inside if(indexPath.section == 1), and you had better remember to propagate that change through the five or six other methods of UITableViewDataSource and UITableViewDelegate that you've implemented.