Customizing UITableViewCells: A Better Way.

The techniques and situations described in this article made sense in 2011, but they’re now outdated and mostly unnecessary.

This article describes a technique that allows full table cell customization with great performance and compatibility with Cocoa’s default behaviors.

Customizing UITableViewCells (from now on, “cells”), is something you will eventually do if you use tables in your apps, and need more flexibility than what the standard UITableViewCellStyle* options offer. I’ve come across a number of ways to do this, some good enough for some purposes, some really awful. There are very easy methods that result in terrible performance, and very hard ones that just don’t work in some scenarios. Custom drawing is the way to get performance, but if you don’t do it correctly you’ll break the standard behavior of cells for, e.g., selection and animation.

Apple has its own example. It works very well for the purposes of the demo app, but doesn’t work well in general. Try drawing a custom cell background in the -drawRect: method and you’ll understand. The animation on cell selection is gone. What’s going on?

Another good example comes from Atebits’ blog, where the secret behind the performance and customization of table cells in Tweetie (now Twitter) is unveiled. Well, kind of. That’s just part of the story, but it allowed me to better understand what influences scrolling performance on iOS.

In both this examples, cell selection behavior is simulated by drawing on a transparent background, at least when the cell is in a selected state. This way, the blue selected background is visible underneath the custom drawn content. This doesn’t work well with the deselection animation, though: if you look closely, you’ll see that the custom drawn content does not animate from the selected to the not selected state. This isn’t a great problem if you’re just drawing text, but for some kinds of content this flashing can be really distracting or just plain ugly. It also involves animation with transparent views, which might not be fast enough in some scenarios.

Can We Do Better?

To start off, we need to clearly understand how cell drawing works. Please note that this article only applies to plain cells. Grouped cells are another story, but if you understand the technique proposed here and study Apple’s documentation, you’ll be able to customize those as well.

Two views are involved: the Content View and the Selected Background View. Both are properties of any UITableViewCell object.

This animation is very important when used in combination with the ubiquitous navigation controllers: when the user taps the back button, it lets him know where he was coming from, making his navigation easier. That’s why Apple’s Human Interface Guidelines recommend it.

Apple also recommends to use as few views as possible, and to keep them fully opaque. This is a huge performance boost, since iOS can avoid compositing different transparent views – a slow operation.

Ideally, we should just use one, custom-drawn opaque view for our cells. That’s what we’re going to do! This means we’re going to draw background and content on the contentView. Do you see the problem? Yes, no animation! iOS will simply redraw your view at the end of the selectedBackgroundView animation, which will be invisible because it’s completely covered by our opaque contentView. What can we do? The simple answer is: animate the cell ourselves.

Here’s the steps we’ll follow:

  1. We’ll properly subclass UITableViewCell and make it use our custom view.
  2. We’ll write drawing code for our custom view: it will draw the background and the content, properly reflecting our cell’s selection state.
  3. We’ll override the -setSelected:animated: method of UITableViewCell and use it to perform our custom animation.

Subclassing UITableViewCell

This is the easy part. Create a UITableViewCell subclass, and override the -initWithStyle:reuseIdentifier: method, adding something like this:

CGRect viewFrame = CGRectMake(0.0, 0.0,
 self.contentView.bounds.size.width,
 self.contentView.bounds.size.height);

self.customView = [[[CustomTableViewCellView alloc]
 initWithFrame:viewFrame
 cell:self] autorelease];

[self.contentView addSubview:self.customView];

As you can see, we’re passing the cell object to the view. This will enable us to understand what state the cell is in when drawing. Our custom view is added to the contentView as a subview, filling it entirely. Since all content fields (imageView, textLabel…) of the superclass are nil until assigned a value, and we have an opaque view covering everything, the only thing the system will actually draw is our custom view, resulting in super smooth scrolling.

Creating a Custom View

Subclass UIView. Make sure you have a “constructor” (init method) that:

  1. Accepts a UITableViewCell as a parameter and stores it in a class property for later use.
  2. Sets the view’s opaque property (self.opaque) to YES.

Override the -drawRect: method and add your custom drawing code, it may look like this:

if (self.cell.selected || self.cell.highlighted) {
  // draw the cell (background and content)
  // in the selected/highlighted state
} else {
  // draw the cell (background and content)
  // in the normal state
}

Notice we’re giving the same meaning to the selected and highlighted properties. In some situations, you might want to do something more sophisticated with them.

Animating Between States

First, in our UITableViewCell subclass, we’re going to override the -setSelected:animated: method, with code like this:

if (animated) {
  // animation code
  [super setSelected:selected animated:NO];
  // more animation code
} else {
  [super setSelected:selected animated:NO];
}

As you can see, we’re always making the superclass change its selection state without animation. We don’t want that animation interfering with ours, and wasting resources.

For the actual animation (remember, it’s a crossfade from the normal state to the selected state), we’re going to use a bitmap technique. What we’ll do is:

  1. Take a bitmap “screenshot” of the current cell appearance (not selected)
  2. Call [super setSelected:selected animated:NO] – this causes our cell to be redrawn in the “selected” state
  3. Take a bitmap “screenshot” of the current cell appearance (selected)
  4. Create two UIImageView objects and initialize them with the two bitmaps captured in steps 1 and 3
  5. Add both UIImageView objects as subviews of the contentView
  6. Create an animation which fades in the second UIImageView object while fading out the first one
  7. At the end of the animation, remove both UIImageView objects from the contentView

The “screenshot” code will probably look like this:

[self.contentView.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage* bitmapSelected = UIGraphicsGetImageFromCurrentImageContext();

The animation code will probably be similar to the following:

bitmapSelectedView.alpha = 1.0;
bitmapDeselectedView.alpha = 0.0;
[UIView beginAnimations:@"deselect" context:nil];
[UIView setAnimationDuration:0.5f];
[UIView setAnimationDelegate:self];
[UIView setAnimationDidStopSelector:@selector(animationDidStop:finished:)];
bitmapSelectedView.alpha = 0.0;
bitmapDeselectedView.alpha = 1.0;
[UIView commitAnimations];

In your -animationDidStop:finished: method, you will call -removeFromSuperview on both bitmapSelectedView and bitmapDeselectedView. I suggest you use tags for this purpose, instead of retained class properties. Even better, if you’re targeting iOS 4.0 and above, use the block-based animation methods of UIView.

I’m sure you can figure out the rest on you own.

Conclusions

I hope this will help some of you out there. I know I struggled to find something like this, and had to develop my own solution. These kind of custom cells are a nice addition to my apps, allowing for flexibility and performance.

An alternate technique might involve using two views, one as a subview of the contentView and another one as a subview of the selectedBackgroundView. This might allow full customization with good enough performance for most scenarios, and it might be a little easier to understand. But I think the approach described above is both simpler and faster. If you don’t want to have drawing code for the background and the content in the same class, just do like I do, and create a hierarchy of UIView subclasses.

Note: this technique doesn’t work in the situations where Cocoa resizes your contentView, of course. The table background is visible around your now smaller contentView. So you’ll also have to implement a custom “edit mode” and a custom “index bar” if you’re planning to use these features.

If you have any questions, feel free to contact me, and I’ll get back to you!