I'm struggling to come up with an efficient and performant way of loading remote images (over a local wifi network) to be used as images in a UITableViewCell.
What I'm basically trying to achieve is something similar to what the Apple iTunes Remote app does - when you browse a list of albums, it retrieves the artwork from the machine running iTunes and displays it on the left. The Remote app obviously does this asynchronously as the artwork doesn't appear immediately the first time you view a row.
Here's what I have so far: a custom UITableViewCell subclass that represents an Album; it has two properties, album name and album artwork URL, and two views, a UILabel and a UIImageView. When the album name is set, the UILabel is updated. This works just fine. When the artwork URL is set, the idea is that an asynchronous HTTP connection downloads the artwork an dupdates the UIImageView when its finished.
Here is my code for a custom AsyncArtworkFetcher class which, given a URL, downloads the artwork asynchronously and invokes a method on a delegate object when finished:
Code:
@implementation AsyncArtworkFetcher
@synthesize delegate;
@synthesize url;
@synthesize userData;
- (void)fetch;
{
NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:5.0];
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
if(connection) {
receivedData = [[NSMutableData data] retain];
}
}
#pragma mark NSURLConnection Delegate Methods
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
[receivedData setLength:0];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
[receivedData appendData:data];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
UIImage *downloadedImage = [UIImage imageWithData:receivedData];
SEL delegateSelector = @selector(artworkFetcher:didFinish:);
if([delegate respondsToSelector:delegateSelector]) {
[delegate performSelector:delegateSelector withObject:self withObject:downloadedImage];
}
[receivedData release];
[connection release];
}
@end
And here's the relevant part of my custom table view cell class:
Code:
- (void)setArtworkURL:(NSURL *)newURL;
{
if(newURL != artworkURL) {
[artworkURL release];
artworkURL = [newURL retain];
AsyncArtworkFetcher *artworkFetcher = [[AsyncArtworkFetcher alloc] init];
artworkFetcher.url = artworkURL;
artworkFetcher.delegate = self;
[artworkFetcher fetch];
}
}
#pragma mark AsyncArtworkFetcher Delegate Methods
- (void)artworkFetcher:(AsyncArtworkFetcher *)fetcher didFinish:(UIImage *)artworkImage;
{
albumArtView.image = artworkImage;
[self setNeedsDisplay];
}
The main issue with this approach is down to the way in which UITableViewCell objects are reused. As you scroll through the list, when a cell is reused it will display the artwork for another album until its updated (its only briefly but it just looks wrong). One approach would be to have a unique cell for each album but this defeats the whole point of reusable cells.
Apart from this one issue, this works well enough in the simulator, but running on my actual iPhone it just locks the phone up, probably because of the number of asynchronous requests it's firing off at once. I guess I need to cache the downloaded image in some way (although I thought NSURLConnection dealt with caching automatically - perhaps I'm not configuring it correctly).
Any suggestions on a better way of going about this? Maybe I'm missing a really obvious technique - it must be possible, the Apple Remote app shows it can be done without any serious performance penalties.