Endlessly Solvable Problems
There is no shortage of common engineering problems that have been solved over and over again, or just once. In either case, if a Good(tm) solution is found, it gets reused wherever it is needed. Others however, get solved over and over again. Not because they are hard or unsolvable, necessarily, but because they are interesting and pose an ever-intriguing challenge to developers. Background loading of images for use in custom UITableViewCells is one such problem.
I have written in the past about image fetching and caching solutions, especially with respect to those images’ use within table cells. Those solutions were decent. They used asynchronous code, tracked cell re-use, cached data in CoreData or as files. Not bad and they worked. But this is a new year, which brings new focus and ideas.
For my latest personal project, once again the familiar need arose to display images in table cells loaded asynchronously from the Internet. I came up with what I think is a pretty elegant solution, which I implemented as a category on UIImage
:
[code lang=”objc”]
@interface UIImage (FF)
+(NSBlockOperation *)imageFromURL:(NSString *)urlString
withCompletionBlock:(void(^)(UIImage *image))completionBlock;
@end
[/code]
[code lang=”objc”]
static NSOperationQueue *_operationQueue = nil;
@implementation UIImage (FF)
+(NSBlockOperation *)imageFromURL:(NSString *)urlString withCompletionBlock:(void(^)(UIImage *image))completionBlock
{
static CGFloat scale = 0;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
scale = [UIScreen mainScreen].scale;
_operationQueue = [[NSOperationQueue alloc] init];
_operationQueue.maxConcurrentOperationCount = 4;
});
NSURL *imageURL = [NSURL URLWithString:urlString];
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
NSData *imageData = [NSData dataWithContentsOfURL:imageURL];
UIImage *image = nil;
if (imageData)
image = [UIImage imageWithData:imageData scale:scale];
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock(image);
});
}];
[_operationQueue addOperation:operation];
return operation;
}
@end
[/code]
The interesting thing about this, I think, is that the method returns an NSBlockOperation
to its caller. This allows the caller to cancel the operation, which is especially useful if the caller is a custom UITableViewCell
class.
When an instance of such a table cell is reused (its -prepareForReuse
method is called), any previously created NSBlockOperation
can be cancelled. For example, you might have the following in your custom table cell implementation:
[code lang=”objc”]
@interface CustomTableCell ()
@property (nonatomic, strong) NSBlockOperation *operation;
@end
@implementation CustomTableCell
…
-(void)prepareForReuse
{
if (_operation) {
[_operation cancel];
self.operation = nil;
}
…
}
// configureCell called form your cellForRowAtIndexPath implementation
-(void)configureCell:(id)foo /* object from which to configure cell */
{
…
self.operation = [UIImage imageFromURL: /* a string */
withCompletionBlock:^(UIImage *image) {
if (image) {
/* code to set a UIImageView instance variable’s image
to the image passed to this block */
}
}];
…
[/code]
At the risk of sounding immodest, what I found really remarkable about this code was how well it worked. I was expecting to have to tweak this to make it perform. But when I tried it with a table view with many results the table view scrolled with absolutely no delay or hesitation, and the images filled in nicely as they became available. I was shocked, but really pleased! And so simple!
An improvement to this code would be to cache the fetched image data under /Library/Caches
. I’ll leave that as an exercise, but I plan to get to it myself eventually.