You’ll want to read this post from 2011: HJCache – we’ve released a comprehensive library for free that makes it easy to use dynamically loaded and cached images in your iOS apps, as per this intro article…
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;
}
What happens when the user flicks through a list of 50+ images? Won’t 50 requests get sent out? What if an older request comes back before a newer one? Wouldn’t this load the wrong image in a reused table cell?
Forgive my ignorance about the second part of my comment. The [connection cancel] will take care of table cell reuse and incorrect images.
Hey Mike, Yes and…you got it, the image load gets canceled. It just works when you flick through lots of images. The URL loading system gets called once for each image, and it has the responsibility to queue requests, handle the order, decide how many to run in parallel. We don’t have to code a bit of it. It’s all business as usual for the URL loading system, because it’s built to be able to load all the graphics and included files when you open a new web page.
I’ve updated the article with some UITableViewController code so you can see how I made it work with recycled cells. The images do load out of order, but the callbacks do go to the right AsyncImageView objects. Each image and each table cell has a different object instance of AsyncImageView, its the UITableViewCells that are being recycled.
Any reason why you didn’t just subclass UIImageView instead? I thew together a little demo that uses a UIImageView and has one extra function with the signature: (void)loadImageFromURL:(NSURL*)url shouldCacheImage:(BOOL)cacheImage
I think you can figure out what it does, but if you cant it caches all images to disk automatically and pulls them from disk if they exist on load. If you’re interested I can shoot the code over to you…
Using UIImageView is probably even better. Please do send me the code, or post it in a comment here if you like.
Great idea. I have come up with a similar solution (subclassing UIImageView), though I’m liking yours better for the most part. I’ll add my own caching implementation to it to finish it off.
I have been using an NSOperationQueue to load up all of the connections so I can control how many are running at once.
Lastly, you could use this function to shorten a piece of your code:
frame = CGRectMake(0, 0, 75, 75);
Thanks for the tip. Readers, if you are curious about CGRectMake and other helper functions, Erica Sadun has a couple of overview postings.
Hi Mark, could you post your AsyncImageView.h file please? I’m sure I’m missing something in trying to get this to work. Thanks!
Wow, this is great! Thanks for this :-)
I have updated the article with links to source code for AsyncImageView.
Mike or Mark, I’m interested in how the caching was done, perhaps it was saved by URL to a certain directory? Can you post a link to this code sample?
I think you need to think about using some apostrophes. Apps should be app’s, then your meaning would be clearer. Can’t be harder than coding Objective-C, can it?
Hi – great work – thanks for sharing!
One thing though – for me the initial set of images load fine but when I scroll the new incoming images only appear when the scroll animation stops completely. I’m stuck with empty space in all the cells until the animation comes to a complete stop and then all the images appear.
Any idea what’s going on?
Thanks!
Same for me. I guess the main thread is busy scrolling, calling methods on the UITableViewController. I think that the user experience is OK for my app – if the user expects the thumbnails to load it seems natural to stop scrolling for a moment and wait and see what loads. You can of course add text to the cell (title of the image in my case) that displays right away.
And thank you ;)
I think that the download can start right away after initWithRequest. The documentation doesn’t state if this method starts the download or not, but it seems that it does.
connection = [[NSURLConnection alloc]
initWithRequest:request delegate:self];
An excellent tutorial! How might I go about implementing a simple caching system to hold the images?
Hai,
I read out ur tutorial… It’s good. I am also need Asynchronously loading image into TableView. If u send the entire code( .h, .m and xib files) then its very easy to understand ( i am very new to this xcode platform)
Shawn and Devi, thanks for your comments, glad this is helpful. I’m sorry but I can’t post an example app at this time. The class will work anywhere a UIView would go, so you can indeed use it in places other than a table, like a cover flow screen. Good luck :-)
I just started on iPhone development and this was a great article. I apologize if this is seems like a silly question. However, one place where I think this asynchronous implementation is not useful is when a user needs to login to an app. When my app sends the username/password to a web server, I need this call to be synchronous so that the authentication can be done after the response from the web server is received. Since the call is asynchronous, my authentication always fails. Any ideas on how to make this a synchronous call?
I am developing a game that uses Facebook Connect to query for friends who are also playing the game. We have leaderboard support — so you can query for your friends’ high scores — and I spent most of today trying to wrangle some code to load images into a custom UITableView from the Facebook servers. I was loading synchronously just to try and get things running and was working on adjusting the code so that I could switch to an NSURLConnection-on-demand type setup ..
And then I found this article. Very nice code. I dropped it in and had it up and running in 15 minutes.
Sincerely, big thanks!
This is fantastic stuff, and I had no problems hooking it up to my code. I do have a few questions though (I’m a relative iPhone noob so be gentle please)…
Is there any way to hook this in with the standard cell.image mechanism? That would keep the images from disappearing and reappearing whenever the cells scroll in and out of view.
Also, how would I go about formatting the text so it doesn’t get clobbered by the image? I guess I make a second UIView for just the text and lay that in the cell manually? Again, if I could use cell.image instead that would be a non-issue as the general table stuff would take care of that.
Thanks for the code!
Mark, thanks for your great article. It have helped me a lot.
I’m trying hard to improve your code here to do the following:
1. Have images refreshed while scrolling.
I can’t understand why don’t they – tried making [cell setNeedsLayout] and all that.
2. Cache images on disk so they don’t load again every time.
Tried changing caching option for NSURLRequest and I think it seems that I’ll need to implement that mechanism from URLCache example app.
Let me know if you have any progress on these or any ideas at all.
Thanks for your post again – it’s by far most useful one from what I’ve encountered searching for various iPhone articles.