Skip to content

Load images asynchronously in a UITableView using GCD (Grand Central Dispatch)

January 8, 2012

Recently, I needed to create a UITableView with UITableViewCells with the image property of the cells’ imageView property set to images retrieved from the web.

First, I didn’t want the images retrieved from the web until the tableView sent the message -tableView:cellForRowAtIndexPath: to the dataSource. Second, once the image was retrieved a first time, should the tableView send the message again for the same row, I did not want to retrieve the image again.

To solve the first requirement, I simply set the image in the dataSource method

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell"];
    if (!cell) {
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault 
                                       reuseIdentifier:@"Cell"] autorelease];
    } NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:@"http://www.images.com/images/1"]];
    [[cell imageView] setImage:[UIImage imageWithData:imageData]];
    [[cell textLabel] setText:@"My Beautiful Image"];
    return cell;
}

That was easy enough. Now I know what you’re thinking (or should be thinking), loading images this way is not asynchronous. And you’re absolutely right. While investigating the easiest way to do this, I found several solutions including some that involved using web views. However, what I found the easiest to implement was to use Apple’s Grand Central Dispatch or “GCD.”

First I created a dispatch queue in the init method of the class I am using as my dataSource:

- (id)init
{
    if (self = [super init]) {
        imageQueue_ = dispatch_queue_create("com.company.app.imageQueue", NULL);
    }
    return self;
}

Don’t forget to use proper memory management techniques on the image_queue. I will leave that as an exercise to the readers.


Next, I changed my dataSource method as follows:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell"];
    if (!cell) {
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"Cell"] autorelease];
    }
    NSMutableDictionary *record = [_records objectAtIndex:[indexPath row]];    
    dispatch_async(imageQueue_, ^{
        NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:[record imageURLString]];
        dispatch_async(dispatch_get_main_queue(), ^{
            [[cell imageView] setImage:[UIImage imageWithData:imageData]];
            [tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationNone];
        });
    });
    [[cell textLabel] setText:[record title]]
    return cell;
}

So here, I dispatched the block asynchronously to the image_queue I create and then got the dataWithContentsOfURL. At this point the main thread goes on its merry way and returns the cell to the tableView. When the image is loaded, I then created a block that gets queued up in the main_queue, a default serial queue, which sets the imageView’s image property. Then, I told send a message to the tableView to reload the row at the index path.
That takes care of the asynchronous portion of the tutorial, but don’t go away yet. I still had another requirement—”caching” the images. I put caching in quotes because I cheated and never had to create any real sort of cache. The objects of my array are all instances of NSMutableDictionary. So what I did was to simply add the image to the dictionary as a key value.

Added Thanks to a comment from JackTheVain, I decided to look at an issue I’ve seen occur a couple of times. That is, the image for a row is sometimes the wrong image. This is because inside the GCD block, I am setting the image of the cell. However, cells are reused so the cell that was being used to represent a particular object might be used to represent a different object by the time the message is sent to the cell. Well that line isn’t even needed. We’re telling the tableview to redraw the row after the image is received and now the image is in memory. So I’ll be deleting that line.

Thus my dataSource method finally looked something like this:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell"];
    if (!cell) {
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"Cell"] autorelease];
    }
    NSMutableDictionary *record = [_records objectAtIndex:[indexPath row]];
    if ([record valueForKey:@"actualImage"]) {
        [[cell imageView] setImage:[record valueForKey:@"actualImage"]];
    } else {
        dispatch_async(imageQueue_, ^{
            NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:[record imageURLString]]];
            dispatch_async(dispatch_get_main_queue(), ^{
                [record setValue:[UIImage imageWithData:imageData] forKey:@"actualImage"];
                [[cell imageView] setImage:[record valueForKey:@"actualImage"]];
                [tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationNone];
            });
        });
    }
    [[cell textLabel] setText:[app valueForKey:@"name"]];
    return cell;
}

Thanks for reading. Hope someone found it useful.

Advertisements
25 Comments
  1. Good stuff man. Thanks!

  2. Firdous permalink

    good stuff

  3. jackthevain permalink

    Thanks Daniel. Got me a good start. When my images load, they change multiple times across different images before settling on the final/correct one.

    I suspect something to do with multiple threads on one queue getting the thumbnails, and multiple threads on a separate (main) queue setting the image on the cell, and some wires crossing in between somewhere.

    Any idea why this is happening? Thanks again.

  4. Arun Dhwaj permalink

    Thanks,

    Good Points to fixed the correct images in correct row after downloading the images.

  5. Hi thanks for the great tutorial. But I just want to know when that code goes into else part??

  6. Got it !!! sorry

  7. Ralf permalink

    dispatch_async(imageQueue_, ^{
    NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:[record imageURLString]]];
    dispatch_async(dispatch_get_main_queue(), ^{
    [record setValue:[UIImage imageWithData:imageData] forKey:@”actualImage”];

    // Here, if tableView was released, how to quit gracefully ?

    [tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationNone];
    });
    });

    • Change
      [tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationNone];
      to
      if (tableView) {
      [tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationNone];
      }

  8. Bill permalink

    Use NSCache instead of NSMutableDictionary.

  9. Jeff permalink

    NSMutableDictionary *record = [_records objectAtIndex:[indexPath row]];

    I get an error on this line stating that _records is an undeclared identifier “did I mean record?” AND that there is no ‘objectAtIndex’ selector for NSMutableDictionary. What am I missing here?

    • Sounds like you don’t have an ivar call _records or a property named records.

      • Jeff permalink

        Pardon my thick-headedness, but what type should the _records ivar be? As I understand it, we create an NSMutableDictionary called record and in it we want to put the image associated with the objectAtIndex:[indexPath row]]. So, should the _records be a declared data structure? A NSMutableDictionary, yes?

      • Table views are usually based on an array of objects. For example, an array of contacts, each having an avatar to display.

        daniel brajkovic mobile software engineer

  10. 赵洪武 permalink

    Hi,I want to know when and where you will release this imageQueue?

  11. Im using this viewDidLoad fired method:

    – (void)loadFlickrPhotos
    {
    // Construct a Flickr API request.
    // Important! Enter your Flickr API key in FlickrAPIKey.h
    NSString *urlString = [NSString stringWithFormat:@”http://api.flickr.com/services/rest/?method=flickr.photos.search&api_key=%@&tags=%@&per_page=5&format=json&nojsoncallback=1″, FlickrAPIKey, @”dog”];
    NSURL *url = [NSURL URLWithString:urlString];

    // Get the contents of the URL as a string, and parse the JSON into Foundation objects.
    NSString *jsonString = [NSString stringWithContentsOfURL:url encoding:NSUTF8StringEncoding error:nil];
    NSDictionary *results = [jsonString JSONValue];

    // Now we need to dig through the resulting objects.
    // Read the documentation and make liberal use of the debugger or logs.
    NSArray *photos = [[results objectForKey:@”photos”] objectForKey:@”photo”];
    for (NSDictionary *photo in photos) {
    // Get the title for each photo
    NSString *title = [photo objectForKey:@”title”];
    [photoNames addObject:(title.length > 0 ? title : @”Untitled”)];

    // Construct the URL for each photo.
    NSString *photoURLString = [NSString stringWithFormat:@”http://farm%@.static.flickr.com/%@/%@_%@_s.jpg”, [photo objectForKey:@”farm”], [photo objectForKey:@”server”], [photo objectForKey:@”id”], [photo objectForKey:@”secret”]];
    [photoURLs addObject:[NSURL URLWithString:photoURLString]];
    }
    }

    and then in cFRAIP I do:

    – (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    UITableViewCell *cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@”Cell Identifier”] autorelease];

    cell.textLabel.text = [photoNames objectAtIndex:indexPath.row];

    //[self.loadingIndicator startAnimating];
    dispatch_async(kfetchQueue, ^{
    NSData *imageData = [NSData dataWithContentsOfURL:[photoURLs objectAtIndex:indexPath.row]];
    dispatch_async(dispatch_get_main_queue(), ^{
    cell.imageView.image = [UIImage imageWithData:imageData];
    [tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationNone];
    });
    });
    return cell;
    }

    But it only displays the images in the cells if I tap on that cell. What am i doing wrong?

  12. Thanks Daniel, thats the first time I used Grand Central Dispatch for asynchronous loading of image in a tableview cell

  13. frank permalink

    hello do you have a sample project you can share?

  14. Not setting the image in my completion handler and instead logging it to a local cache and then triggering a reload at that index path is amazing! Exactly the bug I was looking for a solution to when I came across your post… so I’m definitely glad you posted “Added Thanks to a comment from JackTheVain” b/c I was like you and had all the code written almost exactly the same as you but couldn’t figure my way around that last little bug! Thanks again.

  15. You can find an example for loading image asynchronously on UITableviewCell here

    https://github.com/obyadur-rahman/Who-s-Who

  16. Josh permalink

    By the way, to fix your issue with the images being the wrong ones, you should be implementing prepareForReuse in your cell class, setting up an IBOutlet to your UIImageView, and then setting it to nil in that method. That’s the proper way to handle this case.

    • That’s a good idea. However, using that requires you to subclass UITableViewCell. Also, Apple’s docs state that you should not use that to reset content.

      For performance reasons, you should only reset attributes of the cell that are not related to content, for example, alpha, editing, and selection state. The table view’€™s delegate in tableView:cellForRowAtIndexPath: should always reset all content when reusing a cell.

      • Josh permalink

        I stand corrected. Thanks for pointing out this section of the docs I had overlooked!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: