Asynchronous Image Loading for UITableViewCells

Simmons, Brent Simmons

Unless you do everything you possibly could to stay away from anything related to iOS development, then you’ve at a bare minimum stumpled upon at least one of Brent Simmons articles on the continued development of Vesper. I try to blog somewhat regularly, but the frequency tends decrease the more involved I am with a project. However, with Mr. Simmons, it seems to be the opposite and I’m inspired to do more of the same. Being a devout reader of his chronicals it has been comforting to know that a developer, such as himself, goes through many of the conumdrums of development decisions that myself and I’m sure many others do. In addition, I think that the fact a) Brent is willing to disclose so much information regarding the development b) The great responses from other developers on his approaches are exemplary of how great the iOS/Mac dev commnuity is. The fact is that iOS development is hard and no matter how good you are the answers aren’t always straightforward and there is always something new and interesting to learn.

Asynchronous Image Loading - Hasn’t This Already Been Solved

How many guitarist does it take to play Stairway to Heaven? 100…1 to actually change it and the other 99 to say, Not bad, but I could do it better

I’ve seen many different ways to handle effective asynchronous image dowloading, specifically in the context of UITableView’s. Most of the time this involves some variation of utilizing Grand Central Dispatch, which in this context isn’t very effecient for few reasons.

  1. Not a good way to manage the active queues
  2. You end putting in way too many references to the cell instance in UIScrollView delegate methods
  3. Your controller code becomes too large for it’s own good.

My Version

I’m not including any caching logic because that is a subject for another blog (debate)

Benefits

  1. Utilizes NSOperation and NSOperationQueues
    • You can have as many operations as you want in your cell subclass
  2. Reference to the cell instance via block param
  3. Using UITableViewDelegate methods to cancel any download operation for cells that aren’t onscreen

PhotoViewerCell.h

@class PhotoViewerCell;
@class Photo;

extern NSString * const  PhotoViewerCellIdentifier;

typedef PhotoViewerCell* (^PhotoViewerCellBlock)(void);

@interface PhotoViewerCell : UITableViewCell

@property (nonatomic, strong) Photo *photo;

- (void)loadPhotoDataForCell:(PhotoViewerCellBlock)currentCell
                   withPhotoInstance:(Photo *)aPhoto
                               queue:(NSOperationQueue *)queue;
- (void)cancelDownload;

PhotoViewerCell.m

#import "PhotoViewerCell.h"
#import "Photo.h"

NSString * const  PhotoViewerCellIdentifier = @"PhotoViewerCellIdentifier";

@interface PhotoViewerCell() 

@property (nonatomic, strong) UIImageView *imageView;
@property (nonatomic, weak) NSOperation *fullImageRequestOperation;

@end

@implementation PhotoViewerCell

- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
  
  self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
  
  if (self) {
    
    _imageView = [[UIImageView alloc] initWithFrame:frame];
    
    _imageView.contentMode            = UIViewContentModeScaleAspectFit;
    _imageView.clipsToBounds          = YES;

    [self.contentView addSubview:_imageView];
  }

  return self;
}


#pragma mark - Public Methods

- (void)loadPhotoDataForCell:(PhotoViewerCellBlock)currentCell
                   withPhotoInstance:(Photo *)aPhoto
                               queue:(NSOperationQueue *)queue {
  
  NSOperation *fullImageBlockOperation = [NSBlockOperation blockOperationWithBlock:^{
    
    NSString *fullsizeImageUrl = [NSString stringWithFormat:@"%@%@/iPhoneFullsizeImage",
                                                 [FFManager sharedManager].ff.baseUrl, [aPhoto ms_ffUrl]];
    
    NSURL *downloadURL = [NSURL URLWithString:fullsizeImageUrl];
    NSData *imageData      = [NSData dataWithContentsOfURL:downloadURL];
    UIImage *scaledImage = [UIImage imageWithData:imageData scale:[UIScreen mainScreen].scale];
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
      
      PhotoViewerCell *originalCell = (id) currentCell();
      
      originalCell.imageView.image = scaledImage;
    }];
  }];

  self.fullImageRequestOperation = fullImageBlockOperation;
  
  [queue addOperation:self.fullImageRequestOperation];
  
  self.imageView.image = [UIImage imageNamed:@"placeholder"];
}

- (void)cancelDownload {

  if (self.fullImageRequestOperation) {
    MSLog(@"cancelled");
    [self.fullImageRequestOperation cancel];
  }
}

@end

ViewController Snippet


- (void)viewWillDisappear:(BOOL)animated {
  
  [super viewWillDisappear:animated];
  
  [self.imageDownloadQueue cancelAllOperations];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

  PhotoViewerCell *cell = [tableView dequeueReusableCellWithIdentifier:PhotoViewerCellIdentifier
                                                           forIndexPath:indexPath];

  PhotoViewerCell * (^currentCellForBlock)(void) = ^PhotoViewerCell*{
    return (id) [tableView cellForRowAtIndexPath:indexPath];
  };
  
  Photo *currentPhoto = (Photo *)self.photos[indexPath.row];
  
  [cell loadPhotoDataForCell:currentCellForBlock
                        withPhoto:currentPhoto
                            queue:self.imageDownloadQueue];
  
  return cell;
}

- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
  
  PhotoViewerCell *currentCell = (PhotoViewerCell *)[tableView cellForRowAtIndexPath:indexPath];
  
  [currentCell cancelDownloads];
}