UITables with Downloaded Images – Easy Asynchronous Code

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

postcards-screen1The 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

  1. alloc and initWithRect:
  2. add it to a view, eg in a table cell’s content view;
  3. 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

AsyncImageView.h

AsyncImageView.m

@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;
}

Comments

  1. Eddy says:

    I’m also having an issue with the thumbnailURLAtIndex method? It doesnt’ exit anywhere.

  2. Javier Giovannini says:

    This is still an excelent and much cleaner and simpler method to do this, i was already programming a new thread when I saw this excellent post. Thank you so much.

  3. 成真 says:

    感谢作者

  4. piersoft says:

    please, someone have problems with asyncimageview and .bmp image thumb?

  5. Nik says:

    This is not bad – however, I like to create my table views in XIB files. So I think I’ll do exactly the above, but in a category on UIImageView. Let me know if you’re interested and I’ll send it to you.

    • lokesh says:

      Plz ink could u send me the code… i am using image views in table view to display image from server . I am getting json response of images when i type in localhost/../../jsonFetch.php on my browser but nothing is happening in Xcode. Please help me out./..

    • treat says:

      HI…. it will be really great if you could send over the sample. Kinda stuck in the same situation. anshul.goel@jasperindia.com

  6. Veeramanikandan says:

    I’ve used your code to display bunch of images.. but touchesBegan method not called..

  7. Dinesh says:

    loading asynchronous images in UItableview, they disappear at every scroll??

  8. nafees ahmad says:

    where i can get the full code ?
    actually image Download is showing error for undeclared identifier.

  9. Dan Zeitman says:

    Nice work, I’m wondering if you have created an updated example for ARC or GCD?

  10. raj says:

    In this code where we have to give multiple Urls ..?? to call into nsurlconnection…

  11. Vico says:

    Very nice tutorial for a noob in Ios like me, i did it without real difficulties, except a problem for that line :
    NSURL* url = [imageDownload thumbnailURLAtIndex:indexPath.row]; because i didn’t know that this method “thumbnailURLAtIndex” doesn’t exist and i was only for the example…

    Thank you very much !!!!

  12. Anzor A. Khoutov says:

    Thanx a lot! It’s really simple, at least for using. I guess I can now add caching mechanisms to store semi-static images via CoreData Framework

  13. tom says:

    your code is fantastic

    but i ‘m stuck on how to add the uiactivityindicator during loading . could you help , thx

  14. Gianfry7 says:

    I am having a problem with the code. Images download perfectly, the scrolling is very smooth but when i scroll down the upper images that are not visible on the screen anymore have to reload to make them visible again. I hope you can help me. I can put my code if you want.

  15. xuruixiang says:

    thank you very much!your article is breif.

  16. Arne says:

    Great post, little detail … autoresizingMask should be bit-OR, not logical OR. Use | instead of ||.

Trackbacks

  1. [...] -테이블뷰에 원격이미지를 넣을경우 스크롤이 느려지는 현상- LazyTableImages 샘플http://developer.apple.com/iphone/library/samplecode/LazyTableImages/Introduction/Intro.html#//apple_ref/doc/uid/DTS40009394 AsyncImageView 클래스 http://www.markj.net/iphone-asynchronous-table-image/ [...]

  2. [...] iPhone Asynchonous Table Image: Follow this thorough article to learn about loading multiple images in your iPhone app in an asynchonous manner. [Markj] [...]

  3. [...] 6.ASIHTTPRequest http等相关协议封装 7.EGORefreshTableHeaderView 下拉刷新代码 8.AsyncImageView 异步加载图片并缓存代码 9.类似setting的竖立也分栏程序 [...]

  4. [...] This link helped me when I tried to load asynchronously in Table View thumbnail images: http://www.markj.net/iphone-asynchronous-table-image/ [...]

  5. [...] 6.ASIHTTPRequest http等相关协议封装 7.EGORefreshTableHeaderView 下拉刷新代码 8.AsyncImageView 异步加载图片并缓存代码 [...]

  6. [...] 6.ASIHTTPRequest http等相关协议封装 7.EGORefreshTableHeaderView 下拉刷新代码 8.AsyncImageView 异步加载图片并缓存代码 9.类似setting的竖立也分栏程序  [...]

  7. [...] So I did my assignment and constulted some of my friends if they happened to experience what have been aching my head for days. Unfortunately they haven’t, so I accidentally searched goggle on how other developers perform their image requests and loading. Then I luckily got into a blog which used the Asynchronous image request idea but after trying the classes he made, It simply won’t work for me especially if using table-view. So I read the entire comments of that posts and thank God someone kind posted his link where he created a classes that solved my problem. Thanks to MarkJ for his post. [...]

Speak Your Mind

*