Lazy loading multiple images on background threads on the iPhone

Purpose:

This is a follow-on post in response to a question about the previous post. This entry expands on that post by explaining how to get the iPhone to load multiple images on a background thread (e.g. lazy loading), and updating the main thread with the images once they are retrieved – simple lazy-loading (for a more sophisticated version for images within a UITableView, see Apple’s lazy-loading example).

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: BackgroundThreadLoadMultipleImages.zip

Lazy-loading multiple remote images

See the above mentioned previous post for the background on this. The flow remains roughly the same as the previous example.

Flow of background image loading

Flow of background image loading

In code, the relevant functions are as follows;

In viewDidLoad()

To modify the code from handling just one image to multiple image we need some way to reference the specific image view that is to contain the correct  image after it has been downloaded in the background thread.  In order to do this, a tag (a unique identifier that can later be used to find the UIView object) can be applied to each UIImageView.

The important line to note is the [self performSelectorInBackground:@selector(loadImageInBackground) withObject:arr]; 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.

In the code below, a loop is used to create 5 image views, each one is assigned a unique integer tag.  The tag and the URL of image to download are then passed to a background thread (using method loadImageInBackground()) to download the image.  Since performSelectorInBackground() method only accepts one parameter, the tag and the URL are put in an NSArray and passed as a parameter to the method loadImageInBackground().

- (void)viewDidLoad {
    [super viewDidLoad];

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

	// 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];

	// Create multiple imageviews to simulate a 'real' application with multiple images
	CGFloat verticalPosition = 10;
	int i = 1;
	for (i=1; i<6; i++) { 		 		// Set vertical position of image in view. 		if (i > 1) {
			verticalPosition = verticalPosition+85;
		}

        UIImageView *imageView = [[UIImageView alloc]  initWithFrame:CGRectMake(122, verticalPosition, 80, 80)];
        imageView.tag = i;
		[self.view addSubview:imageView];

		// set the image to be loaded (using the same one here but could/would be different)
		NSURL *imgURL = [NSURL URLWithString:@"http://londonwebdev.com/wp-content/uploads/2010/07/featured_home.png"];		

		// Create an array with the URL and imageView tag to
		// reference the correct imageView in background thread.
		NSMutableArray *arr = [[NSArray alloc] initWithObjects:imgURL, [NSString stringWithFormat:@"%d", i], nil  ];

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

	}

	// clean up
	[pool release];	

}

Add method loadImageInBackground()

The only real difference here from the single-image version previous post is that the method signature now receives an array, the array containing the image URL and the tag of the UIImageView where the image is to be placed.  Note that the tag isn’t used in this method but is passed to the assignImageToImageView() method where the downloaded image is set in the image view.

- (void) loadImageInBackground:(NSArray *)urlAndTagReference  {

		NSLog(@"Received URL for tagID: %@", urlAndTagReference);

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

	// Retrieve the remote image. Retrieve the imgURL from the passed in array
	NSData *imgData = [NSData dataWithContentsOfURL:[urlAndTagReference objectAtIndex:0]];
	UIImage *img    = [[UIImage alloc] initWithData:imgData];

	// Create an array with the URL and imageView tag to
	// reference the correct imageView in background thread.
	NSMutableArray *arr = [[NSArray alloc] initWithObjects:img, [urlAndTagReference objectAtIndex:1], nil  ];

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

}

In assignImageToImageView()

This method receives a downloaded image and a tag in a single array parameter: imgAndTagReference. A loop is created with iterates over all UIImageView subviews on the main view. Each of these subviews has it’s tag checked against the one passed in in the array parameter and if a match is found, we know that we should place the passed in image into that particular UIImageView.

- (void) assignImageToImageView:(NSArray *)imgAndTagReference
{

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

	// loop
	for (UIImageView *checkView in [self.view subviews] ) {
		NSLog(@"Checking tag: %d against passed in tag %d",[checkView tag], [[imgAndTagReference objectAtIndex:1] intValue]);
		if ([checkView tag] == [[imgAndTagReference objectAtIndex:1] intValue]) {

			// Found imageView from tag, update with img
			[checkView setImage:[imgAndTagReference objectAtIndex:0]];

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

			//change width of frame
			CGRect frame = checkView.frame;
			frame.size.width = 80;
			checkView.frame = frame;

		}
	}	

	// release the pool
	[pool release];

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

}

The Result

Multiple images lazy-loaded on background threads

Multiple images lazy-loaded on background threads

Notes

The code takes the previous post which dealt with lazy-loading a single image and adds the ability to handle lazy-loading of multiple images. Some improvements to the code might be to use an object property to store all the image URLs and tag identifiers in a single array and reference that in the functions instead of passing the individual arrays around.  Also, looping through all the image views to identify the correct one might be better done in another way that I’m not aware of. The activity indicator could/should be replaced with one indicator for each image.

The code 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. This is done in the complete example available for download here. BackgroundThreadLoadMultipleImages.zip


Facebook Twitter Email

Tags: ,

4 Responses to “Lazy loading multiple images on background threads on the iPhone”

  1. Piyush 02. Jun, 2011 at 9:19 am #

    nice example. but i want to display all the images in uitableviewcell

    Please help me if you can.

    Thanks

  2. Nir 08. Jul, 2011 at 1:33 am #

    Hi. Nice example. I used it in my application and its performance is great.
    But I have one query. What will happen if the image downloading is going on and the ViewController gets deallocated? Will the application crash? I tried to generate this scenario but I could not.

    Also, I am downloading more than 50 images at a time. Do you think it will create memory issues?

    Thanks.

  3. Girish Chauhan 28. May, 2012 at 3:04 am #

    Hi.
    Nice example and best perform of it’s code example

    thank

  4. m_br 06. Apr, 2013 at 12:24 pm #

    Hi,it is really a nice example and very helpfull :) thank you :)
    But i am wondering how may I adapt such example with the segmentedController ?

Leave a Reply