It’s often the case that the apps I am working on are required to download quite a couple, or even a few dozen, images and display them in a UIImageView. In looking online I found a few different methods of loading image data for a UIImageView in the background, and a few examples of caching image data, but they were all overly complicated for my needs, and none that combined the two ideas.
So, I’m posting here my very simply extension of the UIImageView class that provides a way to load an image in the background. It also provides an extremely simply caching system, which works great if you are displaying a list of thumbnails but would need better memory management for a true cache.
You can download the [download id=”6″] here.
The header file is pretty straight forward, it simply defines our additional methods for the UIImageView class.
// UIImageView+Cached.h
//
// Created by Lane Roathe
// Copyright 2009 Ideas From the Deep, llc. All rights reserved.
@interface UIImageView (Cached)
-(void)loadFromURL:(NSURL*)url;
-(void)loadFromURL:(NSURL*)url afterDelay:(float)delay;
@end
Note that both methods assume you already have a UIImageView to work with. While it would be fairly easy to add a method to create a UIImageView as part of the load, I chose not to because all of the places where I wanted to use this were defined in Interface Builder (IB) and hooked up to an instance variable. I do as little interface design in code as possible, even laying out all of my table cells in IB. For me it makes development a lot faster, and interface tweeks and updates not just simpler, but requiring fewer iterations.
loadFromURL: simply takes a URL to any supported image file and sets up for loading the image in the background. When the image finishes loading the UIImageView’s setImage: method will be called to update the view’s image.
loadFromURL:afterDelay: simply delays calling loadFromURL: for the amount of time (in seconds) given as the delay. I’ve used this in programs where I wanted to optimize other parts of the program to display first, but still put the image load call in the same code section (ie, typically in viewWillDisplay in my apps).
Now for the actual code, which is quite small for both a background loader and caching system.
// UIImageView+Cached.h
//
// Created by Lane Roathe
// Copyright 2009 Ideas From the Deep, llc. All rights reserved.
#import “UIImageView+Cached.h”
#pragma mark –
#pragma mark — Threaded & Cached image loading —
@implementation UIImageView (Cached)
Note here that I have hard-coded my cache for a maximum of 50 images. Typically cache systems are limited in the amount of RAM used instead of a count, but for this quick category it fit my needs. Again, if you are loading small images (like icons or thumbnails) then this will probably work fine for you as well, otherwise you probably want to Google for a real caching solution 🙂
#define MAX_CACHED_IMAGES 50 // max # of images we will cache before flushing cache and starting over
I used a static variable in the cache getter to simplify the code. I don’t really like this, but it does mean that I don’t have to worry about calling an init or doing any heavier extension of UIImageView so all in all while it’s not the best solution, it does what it needs quite well.
The method simply checks to see if we have already allocated a dictionary we can use to cache our image references, and if not allocate said dictionary. The method returns the dictionary reference.
// method to return a static cache reference (ie, no need for an init method)
-(NSMutableDictionary*)cache
{
static NSMutableDictionary* _cache = nil;
if( !_cache )
_cache = [NSMutableDictionary dictionaryWithCapacity:MAX_CACHED_IMAGES];
assert(_cache);
return _cache;
}
This is the meet and potatoes of the code here. The first thing it does is look to see if the requested image is in our cache. Note that it does this by the URL ‘string’ returned by the NSObject description method. This is about as simple a check as you can do, because it does not handle any cases where the image changes on the server, there are two identical images with different URL’s, two URL’s resolve to the same image (https and http for instance), etc.
If we don’t find the image in the cache, we setup an allocation pool because we are going to assume that we are not on the main thread, which is true if either of the public methods for this extension are called. We then use the standard NSData method to load the file from the URL and pass that to UIImage to translate that data into something the system can use (ie, the UIImage). After we load a new image, we first check to see if we have available room in our cache, and if not clear the cache out (again, very simple caching here!). Then in either case we can add the image to the cache.
Finally the method calls the UIImageView’s setImage: method to update the view with the new image data. We don’t call it directly because again we are assuming that one of the public API’s were called, in which case we would not be on the main thread, and making any UI calls is therefore bad. Thus, we use the performSelectorOnMainThread: method to call setImage: so that it can be done safely.
// Loads an image from a URL, caching it for later loads
// This can be called directly, or via one of the threaded accessors
-(void)cacheFromURL:(NSURL*)url
{
UIImage* newImage = [[self cache] objectForKey:url.description];
if( !newImage )
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSError *err = nil;
newImage = [[UIImage imageWithData: [NSData dataWithContentsOfURL:url options:0 error:&err]] retain];
if( newImage )
{
// check to see if we should flush existing cached items before adding this new item
if( [[self cache] count] >= MAX_CACHED_IMAGES )
[[self cache] removeAllObjects];
[[self cache] setValue:newImage forKey:url.description];
}
else
NSLog( @”UIImageView:LoadImage Failed: %@”, err );
[pool drain];
}
if( newImage )
[self performSelectorOnMainThread:@selector(setImage:) withObject:newImage waitUntilDone:NO];
}
And finally, the public methods. Each is very simple; loadFromURL: uses performSelectorInBackground: to call the cacheFromURL: method above on a different thread. This allows the main thread (which is running the UI and main loop of your app) to continue on uninterrupted. This slows down the loading a bit, but keeps the user interactions and interface updates nice and smooth, resulting in a better end user experience.
The loadFromURL:afterDelay: method simply uses the afterDelay version of performSelector: to call loadFromURL: after the specified delay. The only purpose here is to provide a way to organize downloads in a very simple, yet usable fashion.
For instance, let’s say I am going to present a list of home for sale, which requires downloading 20 images, and at the same time make 20 web page requests for descriptions of the homes, their prices, etc. We could wait for everything to load, but that is not good end user design (imo). Instead, I can request the UIImageView’s for those 20 homes update their images, but delay that load for 2-3 seconds. Then, I can grab the text data from the website (which typically comes down very fast), and present the view to the user. Thus the user can start reading about the properties and as images come in the view will automatically update the images the user sees.
// Methods to load and cache an image from a URL on a separate thread
-(void)loadFromURL:(NSURL *)url
{
[self performSelectorInBackground:@selector(cacheFromURL:) withObject:url];
}
-(void)loadFromURL:(NSURL*)url afterDelay:(float)delay
{
[self performSelector:@selector(loadFromURL:) withObject:url afterDelay:delay];
}
@end
OK, that’s it. In case you missed it above, you can download the [download id=”6″] here.
Hopefully this will prove helpful to someone 🙂
Update: Take a look at Michael Amorose’s additions to UIImageView’s if mine isn’t your cup of tea.
You must be logged in to post a comment.