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. Mark S says:

    Nevermind, got it

    Just make the cache on disk and save your images there and it’ll be working great
    Use URLCache example’s code

  2. Paul says:

    First, this is a nice solution to one of my apps problems. I just wanted to point out that you need to initialize it with initWithFrame instead of initWithRect as was mentioned in the article(Just like a UIView). Besides that great post.

  3. Lucas Longo says:

    You rock man!! Thanks!

  4. Tobias says:

    Good thing!

    But i can’t get the code to load the first images inside the TableView.
    As soon as i start scrolling, “connectionDidFinishLoading” is called and images start to appear.

    For the first 5 images, “loadImageFromURL” is called, but the delegates will never be reached.

    Do you guys have any idea, why that may be? I didn’t change Mark’s code, just popped it into my App as is.

    Big cheers in advance! :)

  5. Pat says:

    I was about to implement my own async worker infrastructure, because I need to consume a json api without blocking the UI. Then I found this article. It was super easy to apply this to my requirements. Thanks dude, you just saved me approx. 3 days of work.

  6. Peter says:

    Thanks for the article, it solved our performace problem with a scrolling list we need!

    Is there a way to avoid the image reload when the lists scrolls back into view? We cache the image so it’s not fetched from the server the second time but you see it needs to be drawn again. Is there a solution for this?

    I removed the “removeFromSuperview” line which solves the render problem but then it draws some of the images in the wrong cells when reloaded. Any suggestions?

    • markj says:

      The UITableCell is being reused, which is the normal technique for fast/smooth scrolling tables, so you have to redraw the image for each cell. You can keep the image around in memory and just stick it back in a cell as needed, and that can be nice and fast. The image doesn’t show up until scrolling stops though, because of the mode of the run loop (?).

  7. Butch says:

    Good job. I knew I had done this a couple times but didn’t want to look for my old code and adapt it, this was much easier. I took the liberty (per an earlier post) to use UIImageView instead if UIView. Here’s the code. The changes are minor. I removed anything that was manipulating the UIImageView and subviews, dropped the image getter….mostly just change the super class and deleted some stuff.

    //
    // AsyncImageView.h
    // Postcard
    //
    // Created by markj on 2/18/09.
    // Copyright 2009 Mark Johnson. You have permission to copy parts of this code into your own projects for any use.
    // http://www.markj.net
    //

    #import
    #import “Dbg.h”

    @interface AsyncImageView : UIImageView {
    //could instead be a subclass of UIImageView instead of UIView, depending on what other features you want to
    // to build into this class?

    NSURLConnection* connection; //keep a reference to the connection so we can cancel download in dealloc
    NSMutableData* data; //keep reference to the data so we can collect it as it downloads
    //but where is the UIImage reference? We keep it in self.subviews – no need to re-code what we have in the parent class
    }

    - (void)loadImageFromURL:(NSURL*)url;

    @end

    //
    // AsyncImageView.m
    // Postcard
    //
    // Created by markj on 2/18/09.
    // Copyright 2009 Mark Johnson. You have permission to copy parts of this code into your own projects for any use.
    // http://www.markj.net
    //

    #import “AsyncImageView.h”

    // This class demonstrates how the URL loading system can be used to make a UIView subclass
    // that can download and display an image asynchronously so that the app doesn’t block or freeze
    // while the image is downloading. It works fine in a UITableView or other cases where there
    // are multiple images being downloaded and displayed all at the same time.

    @implementation AsyncImageView

    - (void)dealloc {
    [connection cancel]; //in case the URL is still downloading
    [connection release];
    [data release];
    [super dealloc];
    }

    - (void)loadImageFromURL:(NSURL*)url {
    if (connection!=nil) { [connection release]; } //in case we are downloading a 2nd image
    if (data!=nil) { [data release]; }

    NSURLRequest* request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:60.0];
    connection = [[NSURLConnection alloc] initWithRequest:request delegate:self]; //notice how delegate set to self object
    //TODO error handling, what if connection is nil?
    }

    //the URL connection calls this repeatedly as data arrives
    - (void)connection:(NSURLConnection *)theConnection didReceiveData:(NSData *)incrementalData {
    if (data==nil) { data = [[NSMutableData alloc] initWithCapacity:2048]; }
    [data appendData:incrementalData];
    }

    //the URL connection calls this once all the data has downloaded
    - (void)connectionDidFinishLoading:(NSURLConnection*)theConnection {
    //so self data now has the complete image
    [connection release];
    connection=nil;

    //make an image view for the image
    UIImage *img = [UIImage imageWithData:data];
    self.image = img;
    self.frame = self.bounds;
    [self setNeedsLayout];

    [data release]; //don’t need this any more, its in the UIImageView now
    data=nil;
    }

    @end

  8. Butch says:

    Just because I’m having fun with this control, I thought I’d share a couple ideas that might be useful. First off, I added optional delegate methods (i think that’s what they are called, still fairly new to this) so that when I want to, I can be notified that the image finished loading. Another thought I had was that in the event of an error, or the picture could not be found, you could have the control load a local image to indicate that the image could not be found, the way that browsers do. Yet another idea, and I think this may have been mentioned already, you could have the control show a progress indicator while it is loading. If by chance I get all this implemented I might just post the code back here…if i can figure out how to post code and retain the formatting.

  9. Joe says:

    Greetings! I was trying to do something just like this, only with a Category defined off of UIImageView which, in turn, invokes a NSOperation and optionally caches the response. There’s only one problem – if UIImageView goes away, the operation has no way of knowing. The best you can do is retain/release the UIImageView. Of course, that means you’re loading images when you might not need to.

    So this version is intriguing! I’m going to give it a try. However, I’d like to implement caching to memory/disk. I’ve got a few options here. One involves the Apple sample code for URLCache. Another is a combined memory/disk cache located here (only it’s without URL-friendly support): http://kosmaczewski.net/projects/iphone-image-cache/

    Question for those who’ve done it: Is rolling your own cache mechanism overkill? Is the cache used by NSURLRequest persistent across application runs, one of those “set it and forget it” type deals?

  10. markj says:

    Hi everyone. Thanks so much for all the feedback. Now that I’ve used this approach in every subsequent iPhone app it’s very clear just how useful it is. I’ve written a much more sophisticated version for a client, and at some point I’ll be re-writting it again for myself (and may open source it later). Let me tell you what I’ve learned for any of you who are building your own…

    - break the design up into 2 parts…
    ManagedImage – is the part that displays the image & received callbacks about image loading. This goes in your table cells, and can be recycled as the cell gets recycled.
    ImageManager – singleton that helps share images between more than one ManagedImage, manages a local storage cache of images. (I also use a 3rd class – a bunch of ManagedImageRec, one for each image.)
    - Cache images in local storage. Limit cache size using LIFO technique. Record image age by file creation date and image last useage time by file modification date.
    - Cache files should be in the [[NSHomeDirectory() stringByAppendingString:@"/Library/Caches/nameYourCache/"]
    - Shrink cache on app startup and shutdown. easy and quick scan of file ages and deleting files that are too old to keep your cache within set size limit. unless you have a massive cache this is quick and effective
    - Carefully consider the lifecycles of the actual images and the managed image objecrt – the managed image objects get reused in different places in the table, so be sure to remove their previous image before display otherwise cells will show with old images.
    - Using the caching in the URL loading system didn’t seem to work correctly, at least not right now when we have to compile for 2.2 but run on 2.2 and 3.0.
    - If you want super smooth scrolling, you should consider a small memory cache too for your images, but it makes memory management quite a bit more complicated. Test with your images and your tables before writing a memory cache.

    If you’re at company building an app and you need some outside help, Hunter and Johnson iPhone Consultants can help, click on Consulting and Training above.

    Thanks,
    Mark

  11. Yagyesh says:

    I previously faced issue with loading a big image(i.e around 3mb) asynchronously. I tried several ways to load the image so that my app is always responsive but in vain. Irrespective of whether i load it synchronously or asynchronously the app always freezes for some seconds before actually rendering the image.
    But the code above that markj presented made the matter worse. I used this code to load the same image and in addition to app getting freezed it also restarts the device after sometime. Is there any way to load big images and render it on UI smoothly?

  12. @Yagyesh – For this I suggest checking out ASIHTTPRequest over at http://allseeing-i.com/ASIHTTPRequest/

    You can use this to download data directly to disk and then load it into your view as necessary. Might be what you’re looking for.

    @markj – Thanks for the AsyncImageView classes. They’ve given me a great start for a very specific part of an app! :)

    All the best,

    Nikita

  13. deaa says:

    thanks. I tried to add text, but the image appeared above it, I don’t know how to change the text x position ..
    any suggestion?

    thank you

  14. Michael says:

    If using ASyncImageView in a UITableViewCell along with dequeueReusableCellWithIdentifier in the table, when are the pending image loads canceled? I am noticing that if I have a table view with 30 cells and I scroll quickly to the bottom, once I stop scrolling I will see some of the cells load three or four images in succession. This leads me to believe that when the cell scrolls off the screen the image loading is still happening and when that cell gets reused the new image load is being queued up behind it. So, once the scroll stops all the pending image loads for that cell are executed one after another.

  15. Michael says:

    Also, you will eventually get EXC_BAD_ACCESS crashes if you quickly scroll up/down for 20 seconds or so.

  16. Michael says:

    Ah…glanced a bit closer at the AsyncImageLoader code and see the issue: when a new connection request comes in if an existing connection exists it releases the connection. However, it doesn’t cancel the connection before releasing it. So, it won’t be released until after the connection has closed. This is what is causing the image loading in succession in the same cell. This also will cause leaks and eventually crashes. So, the corrected method looks like this:

    - (void)loadImageFromURL:(NSURL*)url {
    if (connection!=nil)
    {
    [connection cancel];
    [connection release];
    connection = nil;
    }
    if (data!=nil)
    {
    [data release];
    data = nil;
    }
    NSURLRequest* request = [NSURLRequest requestWithURL:url
    cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:60.0];

    connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
    }

  17. Great stuff — you saved me a lot of grief.
    I liked the idea of subclassing UIView (like the original
    code), instead of UIImageView so I can pop in
    a UIActivityIndicatorView. Anyway, I was eating lunch
    outside today and watched a yellow jacket repeatedly
    take a chunk of chicken in front of me and took a series
    of pictures. These pictures became the basis for my
    app testing out the async image loader. I added memory
    caching and threw in a UIActivityIndicatorView while
    loading. Anyway, you can find the code here:

    http://ezekiel.vancouver.wsu.edu/~wayne/yellowjacket/YellowJacket.zip

    -Anyway, I haven’t thoroughly tested it yet, but it seems to work
    like a charm. I leave the images up for awhile.

  18. Tim says:

    anybody want to expand this code for me to asynchronously load some text as well?
    i’m grabbing a username from url, a comment from array, and the image from url.

    the image is working smashingly thanks to this thread.
    array works fine too.

    the text still seems to slow down the scrolling.

    i’d appreciate some feedback!
    drop me a line at timfazio@gmail.com

    cheerio,
    tim

  19. Gonso says:

    Wayne, the Yellow Jacket code works great!

    Just one comment, when you scroll the table, since the cells are being re-use the old image appears (with the loading icon) before the corresponding image loads.

    Im trying to figure out where to clear re-used images to avoid this effect, but I can’t figure it out. I tried removing “old” subviews in loadImageFromURL but that didn’t work….

    Any hints/ideas?

    Thanks
    Gonso

    • markj says:

      When a table cell is reused, it still has that old image in it, so that old image should be removed before returning the table cell so that the old image doesn’t appear while the proper image is loading. So in tableView: tableView cellForRowAtIndexPath: if getting a recycled cell, remove the image there.

      The original code does this with : [oldImage removeFromSuperview];

  20. RyanM says:

    Thank you so much this worked great!

Trackbacks

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

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

  3. [...] – 테이블뷰에 원격이미지를 넣을경우 스크롤이 느려지는 현상 – 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/ [...]

  4. [...] A very thorough article including the source code, for loading multiple images from sites like flickr in your iphone app in an asynchronous manner, so that delay is less and user experience is quick and neat. Read it here. [...]

  5. IPhone and iPad Development…

    iPhone and iPad Development Resources References & Books The Pragmatic Programmers have many books and screencasts on doing iphone development: many books and screencasts on doing iphone development…

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

  7. [...] AsyncImageView异步加载图片并缓存代码 [...]

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

  9. [...] 8.AsyncImageView 异步加载图片并缓存代码 [...]

  10. [...] is an alternative to the ever popular AsyncImageView, I decided to make my own version because it lacks some functionality I need. The image view can [...]

Speak Your Mind

*