» Images and UITableViewCells

UITableViewCells are a little tricky. If you want good performance out of them you have to deal with reusing cells. But that isn't enough if you want dynamically downloaded images in your cells. Then you have to deal with HTTP connections, threading and memory issues!

Is there an easy way around all this? Not really, but here are some techniques I use to makes things a little easier. I've created a simple app that grabs video game images from GiantBomb.com's API and throws them into a UITableView. You can check out the entire "TableView-Images" sample here.

Lazy Image Loading

The first thing we need to do is make sure we are only loading games images when necessary, instead of loading all game images on cell creation. UITableViewDelegate has a method that makes this easy -tableView: willDisplayCell: forRowAtIndexPath: This will be called just before our cell is about to become visible.

- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
  [(GameTableViewCell *)cell showImage];
}

Threading

Just downloading the images on demand isn't enough to get a tableview scrolling to be fluid. Downloading the images will lockup the UI while it is happening, so you need to break out some threads!

It's pretty easy, in our custom GameTableViewCell we create a method showImage...

- (void)showImage {
  if ([_game image]) { // If the image has been previously downloaded.
    _gameImageView.image = [_game image];
  }
  else { // We need to download the image, get it in a seperate thread!
    _thread = [[NSThread alloc] initWithTarget:self selector:@selector(downloadImage) object:nil];
  }
}

- (void)downloadImage {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    
    @synchronized(self) {
      [_game downloadImage]; // The game object handles all the HTTP connections... We can just ignore that part      
      [_gameImageView performSelectorOnMainThread:@selector(setImage:) withObject:[_game image] waitUntilDone:NO];
    }
  }
  
  [pool release];
}

This checks to see if the _game object has already downloaded its image. If not it offloads the image downloading to a thread so our app can get back to doing more important things. Once the image is downloaded the thread will update the cell to display the game's image.

@synchronized(self) is necessary because the UITableViewCells are being reused. Since a single UITableViewCell could have created multiple threads updating its content @synchronized(self) ensures that the cells aren't updated out of order.

Waiting Is The Hardest Part

But now we have to wait for a previous thread to finish before we can update a cell image. This is a problem because when a cell is finally updated we have probably already scrolled past it in the tableview!

With some minor tweaks to the code we can fix this.

- (void)showImage {
  @synchronized(self) {      
    if ([[NSThread currentThread] isCancelled]) return;

    [_thread cancel]; // Cell! Stop what you were doing!
    [_thread release];
    _thread = nil;
        
    if ([_game image]) { // If the image has already been downloaded.
      _gameImageView.image = [_game image];
    }
    else { // We need to download the image, get it in a seperate thread!      
      _thread = [[NSThread alloc] initWithTarget:self selector:@selector(downloadImage) object:nil];
      [_thread start];
    }      
  }    
}

- (void)downloadImage {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
  
  if (![[NSThread currentThread] isCancelled]) {
    [_game downloadImage];
    
    @synchronized(self) {
      if (![[NSThread currentThread] isCancelled]) {
        [_gameImageView performSelectorOnMainThread:@selector(setImage:) withObject:[_game image] waitUntilDone:NO];                
      }
    }
  }
  
  [pool release];
}

We start by canceling the current "image downloading" thread, this allows us to use [NSThread currentThread] isCancelled]. Before starting certain tasks we can check to see if the thread was canceled and immediately give control to a waiting thread.

That's it

Those are some simple tips to speed up a UITableView with downloadable images. Make sure you check out the "TableView-Images" sample code here for the full sample code. It includes other helpful tips for memory management.

User Comments

Recent Posts

  • Wax talks to Twitter - October 20, 2009
  • How does iPhone Wax work? - October 19, 2009
  • Setting up iPhone Wax - October 18, 2009
  • Archive

    • Ruby (tinyrb) on iPhone - May 03, 2009
    • Building PCRE static lib for the iPhone - May 02, 2009
    • Amazon EC2 + Chef = Mmmmm - March 29, 2009
    • Images and UITableViewCells - March 19, 2009
    • Objective-c key paths - February 13, 2009
    • POW! - December 26, 2008
    • Abusing Ruby's question mark methods. - November 28, 2008
    • Git hooks make me giddy - November 07, 2008
    • Ruby Equality! equal? eql? == and === - October 26, 2008
    • Ruby, Rails and Google Sitemaps - October 20, 2008
    • Projects

      • Wax Obj-c to Lua bridge for iPhone.
      • Pow a Ruby library for making file & directory manipulation easy.
      • MiniMagick a tiny RMagick replacement.