Loading an image into UIImage asynchronously
I'm developing an iOS 4 application with iOS 5.0 SDK and XCode 4.2.
I have to show some post blogs into a UITableView. When I have retreived all web service data, I use this method to create an UITableViewCell:
- (BlogTableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString* cellIdentifier = @"BlogCell";
BlogTableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (cell == nil)
{
NSArray* topLevelObjects = [[NSBundle mainBundle] loadNibNamed:@"BlogTableViewCell" owner:nil options:nil];
for(id currentObject in topLevelObjects)
{
if ([currentObject isKindOfClass:[BlogTableViewCell class]])
{
cell = (BlogTableViewCell *)currentObject;
break;
}
}
}
BlogEntry* entry = [blogEntries objectAtIndex:indexPath.row];
cell.title.text = entry.title;
cell.text.text = entry.text;
cell.photo.image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:entry.photo]]];
return cell;
}
But this line:
cell.photo.image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:entry.photo]]];
it is so slow (entry.photo has a http url).
Is there any way to load that image asynchronously? I think it is difficult because tableView:cellForRowAtIndexPath is called very often.
I wrote a custom class to do just this, using blocks and GCD:
WebImageOperations.h
#import <Foundation/Foundation.h>
@interface WebImageOperations : NSObject {
}
// This takes in a string and imagedata object and returns imagedata processed on a background thread
+ (void)processImageDataWithURLString:(NSString *)urlString andBlock:(void (^)(NSData *imageData))processImage;
@end
WebImageOperations.m
#import "WebImageOperations.h"
#import <QuartzCore/QuartzCore.h>
@implementation WebImageOperations
+ (void)processImageDataWithURLString:(NSString *)urlString andBlock:(void (^)(NSData *imageData))processImage
{
NSURL *url = [NSURL URLWithString:urlString];
dispatch_queue_t callerQueue = dispatch_get_current_queue();
dispatch_queue_t downloadQueue = dispatch_queue_create("com.myapp.processsmagequeue", NULL);
dispatch_async(downloadQueue, ^{
NSData * imageData = [NSData dataWithContentsOfURL:url];
dispatch_async(callerQueue, ^{
processImage(imageData);
});
});
dispatch_release(downloadQueue);
}
@end
And in your ViewController
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
// Pass along the URL to the image (or change it if you are loading there locally)
[WebImageOperations processImageDataWithURLString:entry.photo andBlock:^(NSData *imageData) {
if (self.view.window) {
UIImage *image = [UIImage imageWithData:imageData];
cell.photo.image = image;
}
}];
}
It is very fast and will load the images without affecting the UI or scrolling speed of the TableView.
*** Note - This example assumes ARC is being used. If not, you will need to manage your own releases on objects)
In iOS 6 and later dispatch_get_current_queue gives deprecation warnings.
Here is an alternative that is a synthesis of the @ElJay answer above and the article by @khanlou here.
Create a category on UIImage:
UIImage+Helpers.h
@interface UIImage (Helpers)
+ (void) loadFromURL: (NSURL*) url callback:(void (^)(UIImage *image))callback;
@end
UIImage+Helpers.m
#import "UIImage+Helpers.h"
@implementation UIImage (Helpers)
+ (void) loadFromURL: (NSURL*) url callback:(void (^)(UIImage *image))callback {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^{
NSData * imageData = [NSData dataWithContentsOfURL:url];
dispatch_async(dispatch_get_main_queue(), ^{
UIImage *image = [UIImage imageWithData:imageData];
callback(image);
});
});
}
@end
Take a look at SDWebImage:
https://github.com/rs/SDWebImage
It's a fantastic set of classes that handle everything for you.
Tim