Table View Cells in Interface Builder

26 April 2010 Table View Cells in Interface Builder

There are a number of techniques for loading instances of UITableViewCell from a nib file to use in your table views; up until recently, I thought that the best way was the Apple recommended way (developer documentation link). It also comes recommended by Jeff LaMarche, who discovered the technique after writing his book, Beginning iPhone Development.

The technique itself isn't a terrible but as the comments on Jeff's article will attest, there is a perception that the technique is hacky, or non-intention revealing. I'm not sure if I'd go as far as calling it a hack, but it certainly lacks enough clarity that somebody not familiar with the technique could find it potentially confusing.

The biggest issue I have with the technique is that it in order to wire up your custom cell with an outlet, your cell nib's file owner needs to be set to the controller that is using the cell. This introduces an unnecessary dependency on the controller and makes it harder to use in other controllers.

An alternative solution, as presented in Jeff's book is to loop through the array of bundle objects returned by loadNibNamed:owner:options to find your cell.

...
if (cell == nil) {
  NSArray *items = [[NSBundle mainBundle] loadNibNamed:@"MyCustomCell" owner:self options:nil];
  for(id item in items) {
    if ([cell isKindOfClass:[UITableViewCell class]]) {
      cell = item;
      break;
    }
  }
}

It's a few more lines of code but it lends itself well to extraction into a custom category on UIViewController.

- (UITableViewCell *)loadTableViewCellFromNibNamed:(NSString *)nibName;
{
  UITableViewCell *cell = nil;
  ... // as above
  NSAssert1(cell, @"Expected nib named %@ to contain a UITableViewCell", nibName);
  return cell;
}

In addition, I've added an assertion to ensure the nib file contains a cell and to fail fast if it doesn't (I can't think of any good reason why you would want this method to ever return nil).

Ensuring cell re-use for performant table views

One problem that both techniques have is that they require the reuse identifier to be set in interface builder; if you forget to do this, your cells will not be re-used and your nib file will have to be loaded for every cell in your table view.

In a recent article, Jeff LaMarche once again offers some advice on how to avoid this problem. It's a reasonable solution but requires a little more boilerplate code than I like - it will also not work if you need to use multiple reuse identifiers, something that Jeff points out in his article.

Instead, I offer an alternative, less intrusive, defensive approach to the problem. In the same way that we can use a simple assertion to ensure our nib file contains a cell, we can use an assertion to check that the cell also has a reuse identifier.

Originally, I added the assertion to the same method but realized that there may be times where you legitimately do not want to set a reuse identifier (e.g. for single-use cells), so I simply introduce a second method in the category:

- (UITableViewCell *)loadReusableTableViewCellFromNibNamed:(NSString *)nibName;
{
  UITableViewCell *cell = [self loadTableViewCellFromNibNamed:nibName];
  NSAssert1(cell.reuseIdentifier, @"Cell in nib named %@ does not have a reuse identifier set", nibName);
  return cell;
}

Now, you still need to set the reuse identifier in Interface Builder, but by using this method we will always fail fast if we forget to do so.

There is also no reason why this technique cannot be combined with Jeff's; if you are using a sub-class of UITableViewCell with your nib-based cell you could easily override the reuseIdentifier method to return a static string but it's up to you.

So after all of this, the table view's tableView:cellForRowAtIndexPath: method ends up looking like this:

UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"MyCustomCell"];
if (cell == nil) {
  cell = [self loadReusableTableViewCellFromNibNamed:@"MyCustomCell"];
}
...

There is still one issue left with this solution: the reliance on a string identifier is still prone to error. A single typo in the identifier in Interface Builder would stop cell reuse from working but would not be picked up by the compiler or the assertions.

This is certainly one advantage of Jeff's solution, which avoids setting the reuse identifier in the nib file completely, but we can still defend ourselves against this problem by introducing a simple and obvious convention: always use the same string for your cell identifier and your cell's nib name. By adhering to this convention, we can add one final check to the category method:

- (UITableViewCell *)loadReusableTableViewCellFromNibNamed:(NSString *)nibName;
{
  ...
  NSAssert2([cell.reuseIdentifier isEqualToString:nibName], @"Expected cell to have a reuse identifier of %@, but it was %@", nibName, cell.reuseIdentifier);
  return cell;
}

Conclusion

Both approaches have their pros and cons. For controller-specific single-use cells, bundling the cells with the nib file for that controller and hooking them up to outlets is probably the simplest solution and is the method I would choose. For general loading of reusable cells, this method seems clearer to me.

The UIViewController category can be downloaded from GitHub.