UITables with Downloaded Images – Easy Asynchronous Code
Readers… do look through the comments if you plan to use this code, other people have posted improvements. Thanks for all the great feedback everyone. MJ
The app ‘Postcards’ from my iPhone developer training class is a utility app for quickly sending a customized postcard, and one thing that makes it super easy is that you can grab pictures from Flickr to include in the postcard design. Postcards makes simple HTTP calls Flickr’s REST API to download public domain images and displays them in a UITableView for the user to pick from. Cocoa Touch makes this all simple and easy to code, and my first development version used synchronous calls to get the images by using NSData dataWithContentsOfURL:
.
.
NSData* data = [NSData dataWithContentsOfURL:[NSURL URLWithString:urls]];
Making synchronous calls to remote web servers from the thread that’s running the apps GUI is of course a bad idea that results in a laggy UI and unsatisfied users. Using synchronous calls in UITableView cellForRowAtIndexPath to load all the images results in a problem six times worse (for 6 rows on the screen) and makes scrolling basically broken as the table won’t scroll until it has the next cell, which it can’t get while the app is waiting for an image to download. Then imagine that on the Edge network! Obviously we need something multi-threaded that can load the images in parallel in the background and update the UI as they finish downloading.
Multi-threaded programming is hard and should be avoided whenever possible, and in this case Cocoa’s beautiful design came to my rescue:
UIView heirachy + URL loading system + delegate design = multi-threaded image loading with no multi-threaded coding!
How can you have your cake and eat it too? Every iPhone app is a multi-threaded program, or at least its running in conjuction with the multi-threaded iPhone operating system. Use the right delegate methods in the right ways, and you can take advantage of extra threads of execution that the iPhone gives you for free without writting any multi-threaded code of your own, hence sidesteping the problem of threading bugs in your code. An iPhone app is one big event loop – your classes have methods that the event loop calls in response to stuff happening on the device and in your app. When you use the URL loading system’s asynchronous APIs, the iPhone uses a different thread than the one running your app’s event loop to load the contents of the URL, and it makes callbacks via your apps event loop when data has been downloaded.
connection = [[NSURLConnection alloc] initWithRequest:request delegate:self]; - (void)connection:(NSURLConnection *)theConnection didReceiveData:(NSData *)incrementalData
Note carefully, when data has arrived from the remote webserver, that other iPhone thread doing the downloading doesn’t make calls into your objects at the same time as your methods are running, it puts messages into your apps event loop. If it called your app directly then chances are your app would be running some UI code or something and you’d have to write thread safe code. Instead, the call that data is ready arrives as an event on the event loop. Events on the event loop run single threaded, one at a time. Using this we can get asynchrous image download from Flickr without writting thread safe code ourselves. Even better, Cocoa’s URL loading system will download those URLs in parallel! For free!
That’s all well and good, but how do you get a table view to update the UITableViewCell with the image after its already been returned? A UIImage is imutable (right?) so you can’t change its image later when the image data has downloaded. Turns out Apple made this super easy too. Instead of putting a UIImage in the UITableViewCell, you put your own UIView object, that is sized correctly for the image you want to display, into the content view of the UITableCell (as a subview). At first your view object it can be empty, or it can have a dummy image in it, or you can pop in one of those spinny ’something is happening’ views. Then when the image data is downloaded, create a UIImageView with the image and pop it in your view in the cell. Hey presto… it appears. While all this is happening the user can be scrolling and going back and forth with a fully functioning UI.
I put this all together in a class AsyncImageView, listed below. It’s use is simple
- alloc and initWithRect:
- add it to a view, eg in a table cell’s content view;
- send it the loadImageFromURL: message.
LoadImageFromURL will return right away, the image will load in the background, and will automatically appear in the view when its finished downloading. The code posted below is something I whipped up pretty quickly (and I didn’t leak check yet!), but hey – parallel, asynchronous image download and display in about 40 lines of code with no thread-safe worries? Works in smooth scrolling tables, even on the Edge network? I rate it a big win, and wanted to share the technique.
I’ve developed an iPhone programming training class that I will be teaching soon in the SFBay Area (though my partners and I could probably bring it to you). It’s very hands on, and specially designed to help professional programmers new to Cocoa and Objective-C over the difficult initial learning curve. In the class we build the Postcards app from start to finish, including AsyncImageView. Email me for more info: markj at markj.net
@interface AsyncImageView : UIView { NSURLConnection* connection; NSMutableData* data; } @end @implementation AsyncImageView - (void)loadImageFromURL:(NSURL*)url { if (connection!=nil) { [connection release]; } if (data!=nil) { [data release]; } NSURLRequest* request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:60.0]; connection = [[NSURLConnection alloc] initWithRequest:request delegate:self]; //TODO error handling, what if connection is nil? } - (void)connection:(NSURLConnection *)theConnection didReceiveData:(NSData *)incrementalData { if (data==nil) { data = [[NSMutableData alloc] initWithCapacity:2048]; } [data appendData:incrementalData]; } - (void)connectionDidFinishLoading:(NSURLConnection*)theConnection { [connection release]; connection=nil; if ([[self subviews] count]>0) { [[[self subviews] objectAtIndex:0] removeFromSuperview]; } UIImageView* imageView = [[[UIImageView alloc] initWithImage:[UIImage imageWithData:data]] autorelease]; imageView.contentMode = UIViewContentModeScaleAspectFit; imageView.autoresizingMask = ( UIViewAutoresizingFlexibleWidth || UIViewAutoresizingFlexibleHeight ); [self addSubview:imageView]; imageView.frame = self.bounds; [imageView setNeedsLayout]; [self setNeedsLayout]; [data release]; data=nil; } - (UIImage*) image { UIImageView* iv = [[self subviews] objectAtIndex:0]; return [iv image]; } - (void)dealloc { [connection cancel]; [connection release]; [data release]; [super dealloc]; } @end
And here is the usage in UITableViewCell. The AsyncImageView gets tagged with 999, and when it gets recycled, that 999 tagged view gets fished out and removed. So only the cell is being recycled, not the AsyncImageView object. When its removed from the cells content view it also gets released, causing dealloc, which in turn cancels the url download (if its outstanding).
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"ImageCell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:CellIdentifier] autorelease]; } else { AsyncImageView* oldImage = (AsyncImageView*) [cell.contentView viewWithTag:999]; [oldImage removeFromSuperview]; } CGRect frame; frame.size.width=75; frame.size.height=75; frame.origin.x=0; frame.origin.y=0; AsyncImageView* asyncImage = [[[AsyncImageView alloc] initWithFrame:frame] autorelease]; asyncImage.tag = 999; NSURL* url = [imageDownload thumbnailURLAtIndex:indexPath.row]; [asyncImage loadImageFromURL:url]; [cell.contentView addSubview:asyncImage]; return cell; }








Hi, this is MarkJ, original writer of this blog post & the simple async image code. It’s been a long time since the original post, and several apps have come and gone, each using this technique, and each making it better. A couple of apps ago this code got a 100% clean re-write to make it more powerful, more robust, easier to use, and in its own separate library we’d like to release for free. Features: async downloading, sync downloading, sharing of images, in-memory FIFO cache that allows ‘load ahead’ of photos in a photo album and automatically cancels loading if you scroll past, and a self maintaining disk cache that can handle 10,000 files and still able to trim itself from getting too full in just a couple of seconds a day, in-cache image automatic image resizing. Neat huh? Sucks that we’ve not released it yet, but we do plan to some time this year. We just have to ship 3 more apps first…
Mean time, I’m thrilled that this blog posts gets so much attention, very happy that its helped so many developers, and sorry for everyone for whom it hasn’t work right first time.
Cheers,
MarkJ
This is so great and just what I was looking for to add images to my RSS parser!
I do get one error: ‘imageDownload’ undeclared (first use in this function).
Any idea what i’m doing wrong? (I imported the code into my tableview).
Thank you!
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = @”ImageCell”;
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc]
initWithFrame:CGRectZero reuseIdentifier:CellIdentifier]
autorelease];
} else {
AsyncImageView* oldImage = (AsyncImageView*)
[cell.contentView viewWithTag:999];
[oldImage removeFromSuperview];
}
CGRect frame;
frame.size.width=75; frame.size.height=75;
frame.origin.x=0; frame.origin.y=0;
AsyncImageView* asyncImage = [[[AsyncImageView alloc]
initWithFrame:frame] autorelease];
asyncImage.tag = 999;
NSURL* url = [imageDownload
thumbnailURLAtIndex:indexPath.row];
[asyncImage loadImageFromURL:url];
[cell.contentView addSubview:asyncImage];
return cell;
}
I think you’re totally wrong about “When its removed from the cells content view it also gets released, causing dealloc, which in turn cancels the url download (if its outstanding).”
If you read the apple doc about NSURLConnection, it says that its delegate is always retained until the download is finished..
Ok. so there is a either a bug or a change with iOS4’s way of handling NSURLConnection. It seems to be that the request doesn’t start because it isn’t running in the main thread. Since the request won’t start, you will just end up with spinners and no images until you start to scroll down, then back up. Here’s the fix:
in AsyncImageView.m, place this code right to the top of the -(void)loadImageFromURL:(NSURL*)url function:
-(void)loadImageFromURL:(NSURL*)url {
if (![NSThread isMainThread]) {
[self performSelectorOnMainThread:@selector(loadImageFromURL:)
withObject:url waitUntilDone:NO];
return;
}
…..
…..
…..
}
I found this code on this webpage:
http://blog.mugunthkumar.com/coding/ios4-issue-nsurlconnection-and-nsoperation/
If anyone wants to enable loading while scrolling simply change NSURLRequest to following in AsyncImageView.m:
NSURLRequest* request = [NSURLRequest requestWithURL: url cachePolicy: NSURLRequestUseProtocolCachePolicy timeoutInterval: 30.0];
connection = [[NSURLConnection alloc] initWithRequest: request delegate: self startImmediately: NO];
[connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
[connection start];
Hi sir
I have written the same code as given above and its working well but a serious problem related to performance issue is that .Suppose we have 15 images to download,now in table 4-5 images are shown initially which will download but if i scroll the iphone to the last images(11,12,13,14,15) then all images before these images are downloaded first.
How can i make table that only current images in table view will be downloaded.
Thanks
Good question deepak – I noticed this too. I was hoping to show 50+ records/images in a table – but now noticing long wait when scrolling fast to bottom – if anyone has ideas on how best to only load content within current viewpoint of table without loading prior images on route would be appreciated.
I imagine this will be tricky to resolve as I believe would need to detect users scroll speed
if scrolling fast then don’t load sequentially just load images in view etc but if scrolling slow load in sequence – wish i knew where to begin to code that but just learning myself at this stage.
Hi Deepak and Lamo, we faced this too in our commercial apps. For the photo browsing part of our facebook app we use a ‘3 photo’ load ahead system so that while looking at photo #4, #5,6,7 are loading too. This code has ‘cancel behind’ too, so that if you scroll ahead quickly it cancels the loading of the photos you passed by. Works nicely. I took that small feature as part of the inspiration of the re-write of the async photo loading code. The new version, which is not yet published here, features a custom circular buffer of images it is loading. The buffer is of configurable size, and it functions to limit how many photos are being loaded at once. As you scroll down the table, images that haven’t finished loading yet (and probably never even started loading) get pushed out of the buffer and their URLs are cancelled. This new library does a lot more, too – caches images in local file storage, shares image objects between multiple table cells, etc.
Thanks for responding Mark!
That sounds really great – don’t suppose you have an ETA for the code update/repost? Sorry normally don’t like to beg but now I’m very anxious to implement what you have described just now :)
Cheers
Thanx markj.. Worked like a charm..