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.
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]; }
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.
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.
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.
Recent Posts