Mobile App Development

Easy Async Image Downloads with GCD on iOS

September 25, 2012

  • Felipe Laso Marsetti
  • 1

It’s probably 99.9% likely that you’ve had to download images from the web in order to populate cells on a table view.

Attempting to do this without any form of threading will more than likely result in one of two things:

  1. Your UI is super slow and unresponsive as it has to wait for each image to download on the main thread
  2. Your app is closed by iOS because the main thread is not responsive

Before the days of Grand Central Dispatch you would have had to setup your own threads and download the images one by one. Possibly even using NSURLConnection and its awful delegate methods.

Fortunately for you there is GCD to easily download the images for a table view cell without locking up the UI.

I’m going to show you a quick way to do this along with caching the images in a dictionary. By doing this you don’t have to download an image every time a user scrolls to a particular cell. The idea comes from work I’ve done with iOS 6’s Social Framework and interacting with Twitter and Facebook.

First, create a mutable NSDictionary instance inside the controller with your table view. Then, for tableView:cellForRowAtIndexPath:, add this code:

– (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
   // 1
   UITableViewCell *cell = [tableView
dequeueReusableCellWithIdentifier:@”Cell”];

     // 2
   NSDictionary *currentTweet = [self.tweetsArray
objectAtIndex:indexPath.row];
   NSDictionary *currentUser = currentTweet[@”user”];
   NSString *userName = currentUser[@”username”];
   if (self.imagesDictionary[userName])

{

        // 3
       cell.imageView.image = self.imagesDictionary[userName];
}
else
{
     // 4

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
^{
            NSURL *imageURL = [NSURL URLWithString:[currentUser objectForKey:@”profile_image_url”]];

         // 5
         __block NSData *imageData;

         // 6

dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
^{
imageData = [NSData dataWithContentsOfURL:imageURL];

                  // 7

[self.imagesDictionary setObject:[UIImage
imageWithData
:imageData] forKey:userName];

                  // 8
                  dispatch_sync(dispatch_get_main_queue(), ^{
cell.imageView.image = self.imagesDictionary[userName];
});
});
});
}

     // 9
     return cell;
}

I’ve added comments in each section so let’s go over them one step at a time:

  1. Dequeue a table view cell from the table view. This method will automatically handle creating a cell in case there isn’t one ready for reuse.
  2. Retrieve the information necessary to setup the cell’s labels and image view.
  3. Check if there’s a UIImage already stored in the dictionary for the current user’s username. You can use other unique identifiers as the key here depending on your data model.
  4. Make a call to dispatch_async on a global queue. You can set the priority to anything besides default if you want. Making it an async call will allow this block to return immediately and not wait for the image download to finish before continuing execution. This is good if you want to avoid locking up the UI and waiting for a response from the block.
  5. Declare an NSData object with the __block identifier. This will make the object mutable inside blocks (otherwise it would be read only).
  6. Inside the asynchronous call to download and process the image, you make a synchronous call (dispatch_sync) to another available background queue with the default priority. This in turn calls dataWithContentsOfURL: to retrieve the user’s profile image at the specified URL and store it in imageData.
  7. Store the downloaded UIImage in your dictionary with the username as the key. Again feel free to change the key to whatever you prefer.
  8. Make a synchronous call on the main thread in order to update the cell’s image view. Remember to ALWAYS make UI calls on the main thread. You may not see any changes or, worse, your app may crash!
  9. Return the cell (this happens long before the image is downloaded).

By making the main call asynchronous you return from the block immediately while it does some processing. In this case you immediately return the cell with the items already populated (labels, accessories, etc).

Inside the async block you perform a synchronous call to get the image. Finally, and with the image downloaded, you make a synchronous call on the main thread to update the cell’s image view.

When trying to determine if you need a call to dispatch_sync or dispatch_async just remember the following differences:

  • dispatch_async returns immediately even if the code hasn’t finished executing
  • dispatch_sync executes all of the code inside the block before returning

Note that you can still lockup the main thread even if you use dispatch_sync on a global queue!

I hope you’ve enjoyed another quick iOS tutorial, and this handy piece of code for you to store as a snippet to reuse across your apps.