iPhone Dev: Simplifying table cell creation

With the iPhone NDA lifted today, I thought I'd celebrate by sharing a little code snippet that has been very helpful as we develop Life Balance for iPhone.
Anytime you create a table on the iPhone, you have to override an UITableViewDelegate method called tableView:cellForRowAtIndexPath:. And in this function you have to test to see if you can reuse an existing table cell before creating a new one. Apple's template code for subclassing UITableViewController starts you off with:
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = @"Cell";
UITableViewCell *cell =
[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero
reuseIdentifier:CellIdentifier]
autorelease];
}
// Configure the cell
return cell;
}
If your table contains cells of different types, then you also have to be careful to use different identifiers for the different cell types, and match the correct identifiers with the correct types. This is all pretty straightforward as coding goes, but it is a little verbose and error-prone if you find yourself replacing one cell type with another.
However, Objective-C is a wonderfully versatile language where it is actually possible to
The header file is named UITableViewCell+Llamagraphics.h, and it looks like:
#import <UIKit/UIKit.h> @interface UITableViewCell (Llamagraphics) + (id) availableCellForTable: (UITableView *)tableView; @end
This declares a new class method called availableCellForTable: that takes a pointer to a table view and returns a table cell, either by reusing an existing cell or creating a new one. The implementation of this routine is in the file UITableViewCell+Llamagraphics.mm, and it looks like:
#import "UITableVIewCell+Llamagraphics.h"
#import <objc/runtime.h>
#include <string>
@implementation UITableViewCell (Llamagraphics)
+ (id) availableCellForTable: (UITableView *)tableView {
const char * className = class_getName(self);
NSString * identifier =
[[NSString alloc] initWithBytesNoCopy: const_cast<char *>(className)
length: std::strlen(className)
encoding: NSASCIIStringEncoding
freeWhenDone: false];
UITableViewCell * cell =
[tableView dequeueReusableCellWithIdentifier: identifier];
if (cell == nil) {
cell = [[[self alloc] initWithFrame: CGRectZero
reuseIdentifier: identifier]
autorelease];
}
[identifier release];
return cell;
}
@end
Using this header file, Apple's starter template reduces to:
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [UITableViewCell availableCellForTable: tableView];
// Configure the cell
return cell;
}
Now doesn't that seem easier? What's more, the method works with custom subclasses of UITableViewCell, so you can use the same method for all of the cell types in your code.
The main trick that availableCellForTable: uses is that for class methods, the variable self is automatically set to the class object that the method was invoked on. So if you call [UITableViewCell availableCellForTable: tableView], self is set to [UITableViewCell class]. If you call [MyTableViewCell availableCellForTable: tableView], self is set to [MyTableViewCell class].
Armed with this knowledge, availableCellForTable: can retrieve the name of the class, convert the name to an NSString, and use the string as the identifier for the cell. There's no danger of associating the identifier with the wrong class, because the identifier is the class name.
If an existing cell can't be dequeued, the code calls [self alloc] to allocate an object of the specified class, and initWithFrame:reuseIdentifier: to initialize it. Again, because of the way that selectors are dispatched in Objective-C, this call will invoke the subclasses' methods if you have overridden them.
If you're developing native apps for iPhone, I hope this trick will make your coding just a little more enjoyable.
