Retrieving remote images on background threads on the iPhone

Purpose:

Explain how to get the iPhone to show an activity indicator while loading a remote image in a background thread, and updating the main thread with the image once it’s retrieved – simple lazy-loading.

SDK Version: 4.2 (although should work on 3+)
Frameworks: Standard view-based SDK libraries: UIKit, Foundation, CoreGraphics
Tested on: Simulator/iPhone 4
Complete code: BackgroundThreadLoadImage.zip A more complex follow-on example with multiple images was posted here.

Loading remote images on the iPhone the lazy (not lazy-loading!) way

If you want to load remote images (images stored on a server over the network) you can load them in the main thread of your view (for example in ViewDidLoad()) with code like the following:

// create a pool
	NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

	// retrieve the image
	NSURL *imgURL = [NSURL URLWithString:@"https://vanappdeveloper.com/wp-content/uploads/2010/07/featured_home.png"];
	NSData *imgData = [NSData dataWithContentsOfURL:imgURL];
	UIImage *img = [[UIImage alloc] initWithData:imgData];

	// create an image view with an appropriately sized frame
	UIImageView *imageViewCover = [[UIImageView alloc] initWithImage:img]; 

	//set contentMode to scale aspect to fit
	imageViewCover.contentMode = UIViewContentModeScaleAspectFit;

	//change width of frame
	CGRect frame = imageViewCover.frame;
	frame.size.width = 276;
	imageViewCover.frame = frame;

	// add the image view as a subview
	[self.view addSubview:imageViewCover];

	// release the pool
	[pool release];

This works fine but the view won’t appear for the user until the image has been downloaded which is quite slow and make the app feel unresponsive, even on WIFI.

Lazy-loading remote images, a better way

The better way to do it is quite simple.  You can still use ViewDidLoad() to start the download but use two methods that operate a background thread to complete the task.  This let’s the iPhone complete the view loading without having to wait until the image has been completely retrieved over the slow (possibly/often dead at least here in Vancouver) connection.    The rough outline is then:

Flow of background image loading

Flow of background image loading

In code, the relevant functions are as follows;

In viewDidLoad()

The important line to note is the [self performSelectorInBackground:@selector(loadImageInBackground) withObject:nil]; which simply initializes a background thread and calls the method: loadImageInBackground(). After doing so the program execution continues, not tying up the interface while waiting for the image to be downloaded.

- (void)viewDidLoad {
    [super viewDidLoad];

	// Assign activity indicator to the pre-defined property (so it can be removed when image loaded)
	self.activityIndicator =  [[UIActivityIndicatorView alloc] initWithFrame:CGRectMake(55, 67, 25, 25)];  

	// Start it animating and add it to the view
	[self.activityIndicator startAnimating];
	[self.view addSubview:self.activityIndicator];

	// Start a background thread by calling method to load the image
	[self performSelectorInBackground:@selector(loadImageInBackground) withObject:nil];

}

Add method loadImageInBackground()

The important line here is the [self performSelectorOnMainThread:@selector(assignImageToImageView:) withObject:img waitUntilDone:YES]; calls the method: assignImageToImageView(). It calls it on the main thread as Apple indicates that UI updates can only be done (safely) on the main thread, hence the use of: performSelectorOnMainThread in that line.

- (void) loadImageInBackground {

	// Create a pool
	NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

	// Retrieve the remote image
	NSURL *imgURL = [NSURL URLWithString:@"https://vanappdeveloper.com/wp-content/uploads/2010/07/featured_home.png"];
	NSData *imgData = [NSData dataWithContentsOfURL:imgURL];
	UIImage *img    = [[UIImage alloc] initWithData:imgData];

	// Image retrieved, call main thread method to update image, passing it the downloaded UIImage
	[self performSelectorOnMainThread:@selector(assignImageToImageView:) withObject:img waitUntilDone:YES];

	// clean up
	[pool release];

}

In assignImageToImageView()

- (void) assignImageToImageView:(UIImage *)img
{

	// Create a pool
	NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

	// create an image view with an appropriately sized frame using the passed in UIImage
	UIImageView *imageViewCover = [[UIImageView alloc] initWithImage:img]; 

	//set contentMode to scale aspect to fit
	imageViewCover.contentMode = UIViewContentModeScaleAspectFit;

	//change width of frame
	CGRect frame = imageViewCover.frame;
	frame.size.width = 276;
	imageViewCover.frame = frame;

	// add the image view as a subview
	[self.view addSubview:imageViewCover];

	// release the pool
	[pool release];

	// Remove the activity indicator created in ViewDidLoad()
	[self.activityIndicator removeFromSuperview];

}

Notes

The code takes the first example and splits it into a few functions, increasing complexity, but very much improving the user experience. It also doesn’t use any third-party libraries or unofficial (read un-AppStore-approvable) functions, and doesn’t leak any memory.

Note that you’ll have to define the activity indicator in the header (.h) file and synthesize it in the implementation (.m) file.

The complete example is available for download here. BackgroundThreadLoadImage.zip

Tags: ,

2 Responses to “Retrieving remote images on background threads on the iPhone”

  1. rg 14. Jan, 2011 at 12:07 am #

    How would one modify this code to work with multiple images, instead of one?

    Thanks.

  2. admin 14. Jan, 2011 at 9:29 am #

    Hi Rg,

    I’ve posted a follow up post with multiple image loading to answer your question here.

Leave a Reply