Tuesday, September 8, 2009

Table View Cells in Interface Builder - the Apple Way™

When Dave and I originally wrote Beginning iPhone Development, the SDK was very much in beta, and a lot of the documentation was incomplete or completely lacking. Given that, I think we did a pretty good job trying to stick with Apple's "best practices" and at guessing at which techniques would later evolve into best practices. In a surprising number of places, we do it the Apple Way™, even though we weren't 100% sure what the Apple Way™ would be when we were writing the book.

We did well, but we didn't bat 1.000.

One place where we missed was in loading table view cells created in Interface Builder.

In the first edition of the book, we hardcoded the index of the cell. That was bad, because when 2.1 shipped, the structure of the array returned by NSBundle changed and our code broke. Whoops!

In the second edition, we changed it so that the code no longer uses a hard-coded index, but instead iterates over the returned array looking for an object of the right class (UITableViewCell). That was a much better approach and works fine.

Somebody at Apple thought of a better way. It's effing brilliant. So brilliant, in fact, that when I first encountered the documentation for it, it took me a while to figure out just what they were doing. Here's the example code from Apple's documentation:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *MyIdentifier = @"MyIdentifier";

UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyIdentifier];
if (cell == nil) {
[[NSBundle mainBundle] loadNibNamed:@"TVCell" owner:self options:nil];
cell = tvCell;
self.tvCell = nil;
}

UILabel *label;
label = (UILabel *)[cell viewWithTag:1];
label.text = [NSString stringWithFormat:@"%d", indexPath.row];

label = (UILabel *)[cell viewWithTag:2];
label.text = [NSString stringWithFormat:@"%d", NUMBER_OF_ROWS - indexPath.row];

return cell;
}

This is what confused me at first:

    if (cell == nil) {
[[NSBundle mainBundle] loadNibNamed:@"TVCell" owner:self options:nil];
cell = tvCell;
self.tvCell = nil;
}

The method loadNibNamed:owner:options returns an NSArray with the contents of the nib, but this code completely ignores that array. It doesn't even capture it. Then, it goes on and blithely assigns some instance variable to another instance variable, and then sets the first instance variable to nil.

Apple's documentation isn't perfect, but usually, it's pretty damn good, so I was pretty sure this code was right, I just wasn't getting something. There was a piece of the puzzle missing.

So, I looked at tvCell, which was declared earlier, and saw that it was an IBOutlet.

“Curiouser and curiouser!” Cried Alice.

Then it dawned on me. The bundle loader automatically connects outlets made to File's Owner when it loads a nib file if the outlet is nil. Notice that when the nib is loaded, the code specifies self as the bundle's owner. So, since this controller class is the File's Owner, when we load the nib, the outlet will get connected to an object in the nib if the outlet with that name on File's Owner is connected to an object in the nib.

So, what's happening is, the cell is created in its own nib file. The File's Owner for that nib file is our table view controller where the code above resides. In the nib file, the tvCell outlet on File's Owner is connected to the custom cell that gets created.

So, after this line of code:
[[NSBundle mainBundle] loadNibNamed:@"TVCell" owner:self options:nil];

tvCellis automatically connected to the custom cell that was created by virtue of the outlet connection in the nib. There's no need to loop or worry about indices. The bundle loader just does it automatically. So, that custom cell is then assigned to cell which is the table view cell that this method returns to the table to display the contents of this row. After that, tvCell is set to nil so that the next time this method gets called, the bundle loader once again connects the outlet for us.

That's just brilliant. Kudos to whomever at Apple thought of it.

If you'd like to see it in action, here's an Xcode project that uses this technique.

0 nhận xét:

Post a Comment

 
Design by Wordpress Theme | Bloggerized by Free Blogger Templates | coupon codes