How to Asynchronously Add Web Content to UITableView in iOS

Matthew Campbell, February 28, 2012

When your iOS app is downloading content from the web, you run the risk of completely locking up your user interface. This makes your app seem to “freeze” and users usually think that it’s broken. Even if it’s just waiting for a response from a web server.

Ick!

Let’s see how to do this better using the Instagram Web API.

Setting the Scene

Imagine that we have an app that uses the Instagram Web API to get and present the most popular images on Instagram right now. An app like that will need to register with Instagram and then use something like NSURLConnection to access the web service. That is what my app, InstaDemo, is essentially doing.

More specifically, InstaDemo has a model class that wraps around the Instagram web API, downloads information about the most popular images and then caches this using Core Data. InstaDemo also presents each popular image on a table view. Users can go to a detail view to see the images close up.

InstaDemo is implemented using Storyboards. Ok, enough with the InstaDemo background. If you want to learn exactly how to implement the web backend and overall architecture of InstaDemo we are going to use this as an example during iOS Code Camp.

The Problem

When you implement an iOS app like this the main issue that you will face is the slow, perhaps unreliable, network connection that you have available. Likely, your app’s table view will stutter and get stuck as it attempts to download and present each image in each table view cell. Take a look at the code below to see how a table view will probably be used to download images from the web.

Can you see the problem?

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    static NSString *CellIdentifier = @"CellOne";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue2 reuseIdentifier:CellIdentifier];
    }

    //make sure the cell content gets reset
    cell.textLabel.text = nil;
    cell.detailTextLabel.text = nil;
    cell.imageView.image = nil;

    InstagramPopularPicture *p = [appModel.listOfPictures objectAtIndex:indexPath.row];

    cell.textLabel.text = p.userName;
    cell.detailTextLabel.text = p.comments;
    
    //Load images from web
    NSData* data = [NSData dataWithContentsOfURL:[NSURL URLWithString:p.imageURL]];
    p.pictureData = data;
    UIImage *thumbnail = [UIImage imageWithData:data];
    cell.imageView.image = thumbnail;

    return cell;
}

I highlighted the code in blue so you can see where we are getting the file. Keep in mind that this is table view delegate method so each time a new table view cell appears we are going to attempt to download an image. If you try to scroll down a table view this method will attempt to download each file on the main thread effectively locking the entire user interface up.

Fix It Using Grand Central Dispatch (GCD) to Download Web Content Asynchronously

Grand Central Dispatch is a technology that let’s you put tasks like downloading files into a queue. Tasks you put into GCD will not execute immediately and they will not interrupt the main thread. What will happen is that iOS will take care of the details needed to run this task in the background. At some point, iOS will execute the code and the task will be complete. GCD is nice because it’s optimized to run well on multicore devices and basically takes care of the business of multithreading so that you don’t have to.

Let’s add GCD to this table view now. I’ll do this in two parts: the first will be to put the file downloading process into a GCD so that the file will download in the background.

//Load images from web asynchronously with GCD
if(!p.pictureData){
   dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSData* data = [NSData dataWithContentsOfURL:[NSURL URLWithString:p.imageURL]];

        p.pictureData = data;
        UIImage *thumbnail = [UIImage imageWithData:data];
        cell.imageView.image = thumbnail;
        [self.tableView beginUpdates];
        [self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObjects:indexPath, nil]
                              withRowAnimation:UITableViewRowAnimationNone];
        [self.tableView endUpdates];     

    });
}
else
    cell.imageView.image = [UIImage imageWithData:p.pictureData];

You can use GCD with C functions. The first parameter is a function that returns a queue (the list where we put tasks that we want iOS to execute when it can). The second parameter is not used currently so you can use but in 0. If you look closely you will see that the third parameter has a ^ which means that it needs a block. Blocks are a way to encapsulate code in Objective-C.

So this gives us most of what we want, the downloading process won’t interrupt the user interface anymore since we are not downloading on the main thread. But, we still want the table view to update when the Instagram image has downloaded. This is something that happens on the main thread so we will want to add another GCD call with the code to update the user interface. We can nest this new GCD call inside the first, but this time we will use the main queue (the main thread and where the user interface works).

//Load images from web asynchronously with GCD
if(!p.pictureData){
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSData* data = [NSData dataWithContentsOfURL:[NSURL URLWithString:p.imageURL]];
        dispatch_sync(dispatch_get_main_queue(), ^{ 
            p.pictureData = data;
            UIImage *thumbnail = [UIImage imageWithData:data];
            cell.imageView.image = thumbnail;
            [self.tableView beginUpdates];
            [self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObjects:indexPath, nil]
                                  withRowAnimation:UITableViewRowAnimationNone];
            [self.tableView endUpdates]; 

        }); 
    });
}
else
    cell.imageView.image = [UIImage imageWithData:p.pictureData];

The Results

Now when you run this app you will see that the table view loads gracefully. If an image is not available, nothing appears until the image is available. More importantly, the table view behaves the way a user excepts and doesn’t lock up when scrolling.

Take a look at the YouTube video linked at the top to see the difference this makes.

Do You Want to Know How To Implement an InstaDemo App?

The demo app that I used is actually doing a lot more that just downloading images. It’s using Storyboards to manage a navigation and table view based interface, NSURLConnection to work with a web API, Core Data to cache web content and of course GCD to implement graceful asynchronous processing. If you would like to see how to do all this for yourself I’ll be covering how to make your own InstaDemo app at the next iOS Code Camp. As a bonus, all attendees will get the source code for InstaDemo!

BTW: space in iOS Code Camp is limited and there is less than a week to register… Click this link to find out more about this event.