Thursday, April 22, 2010

Table View Cells Redux

Quite a while ago, I posted about Apple's recommended way of doing custom table view cells in Interface Builder. The code from that post has been available for 9 months and today, for the first time, somebody pointed out to me that the attached project didn't reuse dequeued cells because I forgot to type the cell's identifier in Interface Builder.

Which is the weakness with Apple's recommended approach. It's a real Achille's heel. It's really, really easy to forget that step, and the code works perfectly fine if you do forget, you're just eating memory and getting poorer performance than you should. Unless you profile your apps or test with very large data sets, you could very well ship your app like this and not even realize it. You never want your customers to discover these things before you.

Now, I didn't even know I had made this mistake until today, but I've known it was a potential problem for quite some time, which is why, in my contract work, I've started using a modified version of the technique. My earlier mistake now gives me a good excuse to post that modification.

Mostly, it's the same technique as I discussed before, only I start out by defining a constant for the identifier. I actually create a header file in my Xcode projects ConstantsAndMacros.h in my project, which I add to my pre-compiled header file. This means that any constants and any macros I put in ConstantsAndMacros.h will be available to all my source code files in my project without having to manually import them. For a simple project, that file might look something like this:


#define TABLE_CELL_IDENTIFIER @"Table Cell Identifier"
#define NSStubLog() NSLog(@"%s", __PRETTY_FUNCTION__)¹

Once I have that file, I add it to the .pch file in the Other Sources folder:

#ifdef __OBJC__
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import "ConstantsAndMacros.h"
#endif

Now that constant is available project wide². Then, in my UITableViewCell subclass, I override the reuseIdentifier method and add a class method with the same name, like this:

+ (NSString *)reuseIdentifier
{
return (NSString *)TABLE_CELL_IDENTIFIER;
}

- (NSString *)reuseIdentifier
{
return [[self class] reuseIdentifier];
}

By doing this, the system will ignore any identifier I set in Interface Builder, or any value I set in code using the setBundleIdentifier: mutator method. For instances of this particular class, it will always use the same identifier. By creating the class method, I have access to that identifier even if I don't yet have an instance of the class.

For all the other steps in using custom table view cells loaded from a nib, the process is the same as the previous tutorial and it works great. Here's an example tableView:cellForRowAtIndexPath: method using this technique:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 
{
TestCell *cell = (TestCell *)[tableView dequeueReusableCellWithIdentifier:[TestCell reuseIdentifier]];
if (cell == nil)
{
NSLog(@"Loading new cell");
[[NSBundle mainBundle] loadNibNamed:@"TestCell" owner:self options:nil];
cell = loadCell;
self.loadCell = nil;
}

cell.cellLabel.text = [NSString stringWithFormat:@"Row %d", [indexPath row]];
return cell;
}

Notice the NSLog() statement? I can open my table view cell in Interface Builder and set any identifier I want, or set no identifier at all, and it will still re-use table view cells. Run the app, and no matter what you do in Interface Builder, you'll only see a handful of rows loaded from the nib file. After the initial loads, it will just keep reusing the same cell instances over and over. This is far less fragile than having to make sure the identifier in IB and the one in your code match.

There might be times when you don't want to set it up like this - when you need to have multiple identifiers for the same table view class, but those situations will be exceedingly rare. In most practical situations where you are subclassing UITableViewCell, you will want a single identifier for all instances of that class. What you normally won't want is the fragility of having to make sure the value in your code matches the one in IB exactly, especially given that there are no obvious signs that you've forgotten to do it.


1 This macro just logs the name of the method of function that it's placed in when that method is called. I use it whenever I stub out an IBAction method to make sure my connections are all made correctly, hence the name, but it's useful for debugging as well.

2 If you will only use a table view cell in a single controller, you probably don't want it here. In general, I try to create table view cells to be generic enough to be used in more than one controller. Sometimes that's not possible or practical, and in those cases, #define your identifier in the table view controller header instead.

You can find a sample implementation project right here/

No comments:

Post a Comment