MHLazyTableImages – Efficiently Load Images for Large Tables

The iOS Developer Library has a sample project named LazyTableImages that demonstrates how to load images asynchronously for a table with many rows.

The app first downloads the list of top paid apps from iTunes (this is an XML file) and shows the list of apps in a UITableView. Then it downloads the icons for the apps in the background and displays them as they become available. Until the icon is loaded, a placeholder image is visible instead.

Only the icons for the visible rows are downloaded; when you scroll the table to make new rows visible, it downloads the icons for these new rows (but only when you are done scrolling).

Screenshot of the MHLazyTableImages demo project

For a recent client project, I needed to do something similar on several different screens. I took the code from this sample project and rewrote it so it was more generic and could be reused among my different view controllers. Today I decided to refactor this some more and make the code available as an open source component. That component is named MHLazyTableImages and you can download it at github.

To demonstrate how to use this class, I modified Apple’s original LazyTableImages sample. The logic for downloading the images is now handled by the MHLazyTableImages and MHImageCache classes. The table view controller only has to create an instance of MHLazyTableImages and connect its data model and table view to it.

To put an image into a table view cell, you simply call -addLazyImageForCell:withIndexPath:. This will first see if the image is already present in the cache and will download it if not.

- (UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath
{
	UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:...];
	if (cell == nil)
		cell = [[[UITableViewCell alloc] initWithStyle:...];
 
	cell.textLabel.text = ...;
	[lazyImages addLazyImageForCell:cell withIndexPath:indexPath];
}

Of course, you need to tell MHLazyTableImages about the URL for the image. That happens in a delegate callback method:

- (NSURL*)lazyImageURLForIndexPath:(NSIndexPath*)indexPath
{
	AppRecord* appRecord = [self.entries objectAtIndex:indexPath.row];
	return [NSURL URLWithString:appRecord.imageURLString];
}

I used a delegate — rather than telling MHLazyTableImages directly what the URL should be for the cell — in order to accommodate scrolling. While scrolling, we don’t want the images to load yet. We will defer downloading until the user stops scrolling. What that happens, -lazyImageURLForIndexPath: is automatically called for the newly visible rows.

There are a few more things that need to happen to make it all work, but that is the gist of it.

Just for fun, I also replaced the original networking code with ASIHTTPRequest. I was already using this class in MHImageCache, and it allowed me to use blocks instead of delegates. That means the demo project will work only on OS 4 and up.

Check out the demo project (RootViewController in particular) to see how everything works in detail.

Comments

  1. Dimitar Chakarov says:

    Thanks, that is a very helpful tutorial! Do you happen to have a version which is ios3.1.3 compatible by any chance? Or any pointers for me if I am to rewrite your code to support it?
    Thanks!

  2. Matthijs says:

    The demo project uses blocks in a fair number of places and those are OS 4 only. However, the original sample code from Apple will show you how to do it without blocks.

    The MHImageCache class that is used internally by MHLazyTableImages also uses blocks. You will have to replace them with delegates. So you’d declare an MHImageCacheDelegate protocol with a method “imageCache:didLoadImage:” and then MHLazyTableImages would implement that protocol.

  3. Dimitar Chakarov says:

    Thanks, I’ll give it a shot

  4. Samuel says:

    I want to put ActivityIndicator on placeholder image but it doesnt work. is there a way to make that for lazy table images?

  5. Matthijs says:

    @Samuel: You will probably have to use your own subclass of UITableViewCell. Instead of placing the placeholder image, tell it to show the activity indicator instead. When an image is loaded, hide the activity indicator and show the image. But you’ll have to modify the code a bit to make this work.

  6. Zhen Hoe says:

    Hi, I was wondering if this is also applicable if I have multiple images in a single cell?

    Not sure if I am in the right direction, but is this the method where I need to updated?

    - (NSURL*)lazyImageURLForIndexPath:(NSIndexPath*)indexPath
    {
    AppRecord* appRecord = [self.entries objectAtIndex:indexPath.row];
    return [NSURL URLWithString:appRecord.imageURLString];

    Thanks!

  7. Matthijs says:

    @Zhen Hoe: The code isn’t really written for multiple images in a single cell. It uses the NSIndexPath to link the image to the cell, so if you have more than one image you will have to use some other class to keep track of this. NSIndexPath just refers to the cell, not to the cell + which image. So you could create a new class, MyIndexPath, that contains an NSIndexPath and an image index, and use that instead. (If your table doesn’t use sections, then you could put the image index in the NSIndexPath’s section property. That might also work.)

  8. Zhen Hoe says:

    @matthijs – thanks a lot! Your code was excellent, and we are super grateful for it. Your advice is noted – will find a way to implement it otherwise. =)

  9. goxy says:

    Well i use lazy images in my project, works great i like it, however seems like postProcess callbacks are called after images loaded (which is also logical :) which causes if user loads controller and immediately goes back (and controller is released) no post processing is done for some images. Can somehow I fix this??

  10. Matthijs says:

    If I remember correctly, the view controller will not be deallocated until those images are loaded because the MHImageCache will hold onto it in its block. However, the view controller sets the delegate of the MHLazyTableImages class to nil, which is why the post-processing does not happen. Perhaps removing the line that sets the delegate to nil will be sufficient to do what you want, but I haven’t actually tested this.

  11. goxy says:

    Thank you! I will try that.

  12. Jonice says:

    Thanks for this article.. On trying.. :)

  13. Tanmay says:

    Any idea how to make it lazy load an image for a custom cell?

    Im not able to control the appearance and position of the cell image. Any idea on how to do that?

  14. Matthijs says:

    @Tanmay Don’t use the built-in cell.imageView property if you’re making a custom cell. Create your own UIImageView and give it its own outlet.

  15. Quang says:

    Hi Matthijs,
    How do I cant remove all CacheImages?

  16. Matthijs says:

    @Quang Do you mean remove the images from the file system? I never actually programmed that. You can simply remove the folder from “Library/Private Documents/Image Cache”.

  17. Dimitris says:

    Hi

    I’ve implemented your classes in my project. My steps for creating the tables are.

    1. Load the JSON file from the server using ASIHTTPRequest
    2. Once the request is completed i parse the response from the server, assign the tableViewData to the JSON Array and reload the table.
    3. Each table is a custom subclass which I layout everything based on the design, and I did implemented the “- (void)didLoadLazyImage:(UIImage *)theImage” so that I place the returned image into my own UIImageView.

    Now the weird thing is that every cell has the same image… The very first one. If I NSLog the imagePath from “- (NSURL*)lazyImageURLForIndexPath:(NSIndexPath*)indexPath” I get a different imagePath.

    Any help would be much appreciated

    Thanks

  18. Quang says:

    @Matthijs
    Will all images in the “Image Cache” be removed when I use function [[MHImageCache sharedInstance] flushMemory] ?

    And

    How can I add one more function: Using an image when it’s existed in DB ?

  19. Dilip Patidar says:

    hi, nice tutorial…
    i used in my app but when i back to controller before image load.
    my app get crash due to delegate responder problem..
    one more problem i get some time app crash on block([self.images objectForKey:key]); point…
    if u have any solution about this problem please provide me..

  20. obaid says:

    Hello, nice tutorial thank you, just stuck at one point, I’ve the background color of the imageview black, and I want to change the color to clearcolor how can I do that in this library ??

  21. Matthijs says:

    @obaid I’m going to clean up this library soon because it’s getting a bit behind the times, and I’ll look into that then.

  22. obaid says:

    I have got the transparent background for he images and its cool. The answer is provided in the Github

Leave a Reply

Your email address is required but will not be visible to others.

 *
 *