Remote Image for iPhone

This code is now a part of the BaseAppKit. See BARemoteImageView class.

In one of my earlier posts I've written about HTML parser for Daily Deviations, an iPhone application that shows feed with daily images from http://www.deviantart.com/. I use it to extract links to the images, their descriptions and other pages. It's easy to show textual descriptions but images require more elaborate handling. Naive approach is to take a list of urls and load them in a loop, but it leads to the following problems:

  • UI gets blocked until all images are loaded. For slow connections application may become unresponsive for a long period of time and users may decide to just shut it down.
  • There may be not enough memory to keep all the images. In the middle of the loop application may be shut down by the OS because it allocated all the memory.
  • Even if the application did manage to load all the images OS may ask it to free some memory later. With plain UIImages we don't have an appropriate facility to release the images and reload them later when necessary. In case of Daily Deviations I just release all the images and when user scrolls the grid missing images got loaded on demand.

Standard solution in this case is to create a proxy object that will load and release the image representation when necessary. For this purpose I've created the RemoteImage class. When the image is not available the proxy may return some default image to indicate that the remote image is not loaded yet or is missing.

Helper Classes

RemoteImage class is pretty generic but does require a couple of helper classes. The first one is NetworkActivityController: its purpose is to wrap access to the network activity indicator. System network activity indicator is a tiny animated icon that is displayed by OS in the system bar. Applications should set it visible when they transmit data over the network. The purpose of the NetworkActivityController is to count a number of network operations so after the first request indicator is made visible and on completion of the last request it is hidden.

//
//  NetworkActivityController.h
//

#import <Foundation/Foundation.h>

@interface NetworkActivityController : NSObject {
  NSUInteger level;
}

+ (NetworkActivityController *)sharedController;
- (void)startActivity;
- (void)stopActivity;
- (void)stopAllActivity;

@end

//
//  NetworkActivityController.m
//

#import "NetworkActivityController.h"

@implementation NetworkActivityController

+ (NetworkActivityController *)sharedController {

  static NetworkActivityController *instance;

  if (!instance) {
    instance = [[NetworkActivityController alloc] init];
  }
  return instance;
}

- (void)startActivity {
  if (++level == 1) {
    [[UIApplication sharedApplication]
      setNetworkActivityIndicatorVisible:YES];
  }
}

- (void)stopActivity {
  if (level == 0) {
    return;
  }
  if (--level == 0) {
    [[UIApplication sharedApplication]
      setNetworkActivityIndicatorVisible:NO];
  }
}

- (void)stopAllActivity {
  level = 0;
  [[UIApplication sharedApplication]
    setNetworkActivityIndicatorVisible:NO];
}

@end

The second class is LoadImageOperation. This class extends NSOperation and represents a task to load remote image that could be performed asynchronously. What happens in Daily Deviations when user opens a feed? HTML page is scanned for images' urls and for each image a new LoadImageOperation is created and placed in the queue. OS picks these operations and performs them asynchronously notifying UI when they complete. User sees a grid of images' placeholders that are filled with images as they are loaded. At the same time user is free to scroll the grid and navigate within the application.

//
//  LoadImageOperation.h
//

#import <UIKit/UIKit.h>

#define kImageKey @"image"

@interface LoadImageOperation : NSOperation {
  NSURL *url;
  SEL selector;
  id target;
  NSMutableDictionary *context;
}

- (id)initWithURL:(NSURL *)anUrl
    selector:(SEL)aSelector
    target:(id)aTarget
    context:(NSMutableDictionary *)aContext;

@end

//
//  LoadImageOperation.m
//

#import "LoadImageOperation.h"

@implementation LoadImageOperation

- (id)initWithURL:(NSURL *)anUrl
    selector:(SEL)aSelector
    target:(id)aTarget
    context:(NSMutableDictionary *)aContext {
  if (self = [super init]) {
    url = [anUrl retain];
    selector = aSelector;
    target = [aTarget retain];
    context = [aContext retain];
  }
  return self;
}

- (void)dealloc {
  [url release];
  [target release];
  [context release];
  [super dealloc];
}

- (void)main {
  NSURLResponse *response;
  NSData *data = [NSURLConnection
    sendSynchronousRequest:[NSURLRequest requestWithURL:url]
    returningResponse:&response
    error:NULL];
  if ([self isCancelled]) {
    return;
  }
  UIImage *image = nil;
  if (data) {
    image = [UIImage imageWithData:data];
  }
  if (image) {
    [context setObject:image forKey:kImageKey];
  }
  [target performSelectorOnMainThread:selector
    withObject:context waitUntilDone:NO];
}

@end

Remote Image

RemoteImage class is the implementation of the image proxy that wraps UIImage and manages its lifecycle. It provides the following features:

  • Dedicated NSOperationQueue shared by all remote images. Each LoadImageOperation is performed on this queue. Why this is an advantage is not obvious but if you consider how Daily Deviations operates it becomes clear. Each day site publishes a list of selected images, also known as daily deviations. User can change the date and see the page for that day. If not all images are loaded for the current day yet and user changes the date Daily Deviations cancels all operations in the queue thus freeing the bandwidth for the next batch of images. With a single queue it is one method call: [[RemoteImage queue] cancelAllOperations].
  • Ability to quickly unload all images. Application should use it when memory is low to drop all remote images' data. It's also possible to lock particular images so they are not unloaded. Daily Deviations for example locks the remote image that is viewed in the details screen so thumbnails could be unloaded if the detailed image is too big.
  • Notifications on status change. When image starts loading, is loaded, is unloaded or is failed to load RemoteImage posts a notification using default NSNotificationCenter. All UI components that are interested in the state of the remote image could listen to these events and update themselves accordingly.

Two main properties of the RemoteImage are link and image. Link is the URL of the remote image and can be changed anytime, typically during creation of the RemoteImage instance. Image property is read-only and points to the UIImage instance when loaded. There is no property for the placeholder image since it really depends on UI component but you may add one if necessary.

//
//  RemoteImage.h
//

#import <Foundation/Foundation.h>
#import "LoadImageOperation.h"

typedef unsigned int RemoteImageStatus;
enum {
    kRemoteImageUnloaded,
    kRemoteImageLoading,
    kRemoteImageFailedToLoad,
    kRemoteImageLoaded
};

// Sent only when image is loaded, failed to load or was unloaded.
extern NSString * const RemoteImageStatusChangedNotification;

@interface RemoteImage : NSObject {
  NSString *link;
  NSInteger width;
  NSInteger height;
  UIImage *image;
  LoadImageOperation *operation;
  RemoteImageStatus status;
  BOOL locked;
}

 // url of the image
@property(copy) NSString *link;

// assumed size; typically from img element
@property(assign) NSInteger width, height;

// actual image
@property(readonly) UIImage *image;

// image state
@property(readonly) RemoteImageStatus status;

// if true then image is used and should not be unloaded
@property(assign) BOOL locked;

+ (void)unloadAllImages;
+ (void)cancelLoadingAllImages;

- (void)load;
- (void)loadWithPriority:(NSOperationQueuePriority)priority;
- (void)unload;

@end

//
//  RemoteImage.m
//

#import "RemoteImage.h"
#import "NetworkActivityController.h"

NSString * const RemoteImageStatusChangedNotification =
  @"RemoteImageStatusChangedNotification";

@implementation RemoteImage

@synthesize link, width, height, image, status, locked;

+ (NSMutableArray *)allRemoteImages {
  static NSMutableArray *images;

  if (!images) {
    images = [[NSMutableArray alloc] init];
  }
  return images;
}

+ (void)unloadAllImages {
  for (RemoteImage *image in [RemoteImage allRemoteImages]) {
    [image unload];
  }
}

+ (NSOperationQueue *)queue {
  static NSOperationQueue *queue;

  if (!queue) {
    queue = [[NSOperationQueue alloc] init];
  }
  return queue;
}

+ (void)cancelLoadingAllImages {
  [[RemoteImage queue] cancelAllOperations];
  [[NetworkActivityController sharedController] stopAllActivity];
}

- (id)init {
  if (self = [super init]) {
    status = kRemoteImageUnloaded;
    [[RemoteImage allRemoteImages] addObject:self];
  }
  return self;
}

- (void)imageLoaded:(NSMutableDictionary *)context {
  [[NetworkActivityController sharedController] stopActivity];
  [image release];
  image = [[context objectForKey:kImageKey] retain];
  [operation release];
  operation = nil;
  status = image? kRemoteImageLoaded : kRemoteImageFailedToLoad;
  [[NSNotificationCenter defaultCenter]
    postNotificationName:RemoteImageStatusChangedNotification object:self];
}

- (void)load {
  [self loadWithPriority:NSOperationQueuePriorityNormal];
}

- (void)loadWithPriority:(NSOperationQueuePriority)priority {
  if (operation && [operation isCancelled]) {
    [operation release];
    operation = nil;
    status = kRemoteImageUnloaded;
  }
  if (status != kRemoteImageUnloaded) {
    return; // already loaded
  }
  status = kRemoteImageLoading;
  NSURL *url = (link) ? [NSURL URLWithString:link] : nil;
  if (!url) {
    status = kRemoteImageFailedToLoad;
    [[NSNotificationCenter defaultCenter]
      postNotificationName:RemoteImageStatusChangedNotification object:self];
    return;
  }
  NSMutableDictionary *context = [NSMutableDictionary dictionary];
  operation = [[LoadImageOperation alloc] initWithURL:url
                         selector:@selector(imageLoaded:)
                         target:self
                        context:context];
  [operation setQueuePriority:priority];
  [[RemoteImage queue] addOperation:operation];
  [[NetworkActivityController sharedController] startActivity];
}

- (void)unload {
  if (self.locked) {
    return;
  }
  if (operation) {
    [operation cancel];
    [operation release];
    operation = nil;
  }
  if (image) {
    [image release];
    image = nil;
  }
  status = kRemoteImageUnloaded;
  [[NSNotificationCenter defaultCenter]
    postNotificationName:RemoteImageStatusChangedNotification object:self];
}

- (void)dealloc {
  [[RemoteImage allRemoteImages] removeObject:self];
  self.locked = NO;
  [self unload];
  [super dealloc];
}

@end

Conclusion

RemoteImage is pretty generic and useful class. You may use it freely in your applications and if you will find any errors or make any significant improvements please consider writing me email.

  • Digg
  • del.icio.us
  • Reddit
  • StumbleUpon
  • LinkedIn
  • E-mail this story to a friend!
  • Print this article!

  • Kevin

    Hi, I'm just a beginner...

    I'm trying to figure out how to use it.

    I suppose we should get the image like this:

    -(void) RemoteImageStatusChangedNotification: (NSNotification *) aNotification{
    remoteImage = (RemoteImage *)aNotification;
    anImageView.image = remoteImage.image
    }

    But I cannot receive that RemoteImageStatusChangedNotification event, Am I missing something?

    thnkx
    Kevin

  • http://www.dimzzy.com dimzzy

    Yes, you should subscribe to receive them.

    @implementation XController

    - (void)loadView {
    [super loadView];
    [[NSNotificationCenter defaultCenter] addObserver:self
    selector:@selector(imageStatusChanged:)
    name:RemoteImageStatusChangedNotification
    object:nil];
    }

    - (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    [super dealloc];
    }

    - (void)imageStatusChanged:(NSNotification *)notification {
    RemoteImage *changedImage = [notification object];
    // update UI
    }

    @end

  • hookjd

    Any chance you want to post a sample XCode project that has your class files in it? It looks like it would be really useful, but its a little hard to take in on one long html page. :)

  • Kevin

    Thnkx a lot!!!

    As you wrote, I forgot to register to receive the notifications. It works really nice !!!
    Now I'm trying to use this class to populate the cells in a UITableView, but I find a little bit inconvenient to register for this notification in every cell. I don't think you designed this class for this particular case.

    Any advice?

  • http://www.dimzzy.com dimzzy

    You can register only the table controller, add a tag property to RemoteImage (like in UIView) and store cell index there. When notification comes in you find the cell and update it.

  • http://clocksmith.mggm.net Gary Morris

    Very useful. I've been playing with this, expanding it to fit my needs which are a little different but it's been very helpful to start with this code.

    One thing I noticed in the process is a memory leak. When you release a RemoteImage object after you are done with it, dealloc never gets called. This is because each RemoteImage object is also retained by the static NSMutableArray* images inside the AllRemoteImages method. RemoteImage dealloc tries to remove itself from this array in the dealloc method, but this is too late, it never gets into dealloc because it is retained by the array.

  • http://www.dimzzy.com/ dimzzy

    True; thanks a lot!

  • http://twitter.com/grgcombs Greg Combs

    This looks interesting! Any thoughts on how best to incorporate this into a UITableView with scrolling through cells with images?

  • Dimzzy

    Table cell should observe remote image notifications and update itself when image is available.

  • Naganesh90

    Give some sample to use this RemoteImage... As soon as possible

  • Masterjcl

    Hi can you give as a simple Example use of this class ?

  • http://twitter.com/olavgm Olav Gausaker

    Hi! Very nice, it is just what I was looking for!

  • gupta

    Hi Dimzzy sir,
    how to use this classes please give me one example
    gupta.sunkari@gmail.com
    this is my mail id
    Thanks,
    Gupta

blog comments powered by Disqus