Advanced issues: Asynchronous UITableViewCell content loading done right

Problem?

Haven’t you always wondered why your UITableView is loading “almost” perfectly? I mean, sure- you’ve made it clear to the iOS that all non-trivial cell work (such as downloading images from a remote URL or rendering content) is to be computed asynchronously on a background thread. But sometimes this is not enough, mainly for 2 reasons:

1. Once a cell is out of the visible area, the asynchronous operation you called is still doing work. This often results in unnecessary system resource usage or even buggy table behaviour caused by operations not knowing which cell to return to when they’re done.

2 . UITableViewCells are often reused instances. This means that cells being loaded into the view may sometimes contain data that was loaded originally into a completely different cell. This often causes a “Cell switching” behaviour which can just completely piss you off.

Solved!

First of all it’s important to understand that there are many ways to tackle this issue – some are great ideas and some are, well, awful. The method that I’m about to describe here is based on a demo provided by Apple (namely WWDC 2012 session 211), and you know those guys know a thing or two about iOS.

For our example, I’ll use a simple UITableView instance that is meant for displaying your facebook friends’ names and profile pictures. The main idea is that we start loading the profile images when UITableViewDataSource’s tableView:cellForRowAtIndexPath: is called. If the operation succeeds and the cell is still in view then we simply add the image to the cell’s profile image view (on the main thread). If it’s not – we make sure not to perform the “setImage” part.

Before you start, some prep work: Define an NSOperationQueue for running background operations – in this example, we call it the imageLoadingOperationQueue. Also, define an NSMutableDictionary for storing references to specific operations – in this example we will map the facebook unique ids to the operations on the facebookUidToImageDownloadOperations dictionary.

Most of the important stuff is commented in the code so make sure you read the comments to understand what’s going on:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    FacebookFriend *friend = [self.facebookFriends objectAtIndex:indexPath.row];
    FacebookFriendCell *cell = [tableView dequeueReusableCellWithIdentifier:FB_CELL_IDENTIFIER forIndexPath:indexPath];
    cell.lblName.text = friend.name;

    //Create a block operation for loading the image into the profile image view
    NSBlockOperation *loadImageIntoCellOp = [[NSBlockOperation alloc] init];
    //Define weak operation so that operation can be referenced from within the block without creating a retain cycle
    __weak NSBlockOperation *weakOp = loadImageIntoCellOp;
    [loadImageIntoCellOp addExecutionBlock:^(void){
        //Some asynchronous work. Once the image is ready, it will load into view on the main queue
        UIImage *profileImage = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:friend.imageUrl]]];
        [[NSOperationQueue mainQueue] addOperationWithBlock:^(void) {
            //Check for cancelation before proceeding. We use cellForRowAtIndexPath to make sure we get nil for a non-visible cell
            if (!weakOp.isCancelled) {
                FacebookFriendCell *theCell = (FacebookFriendCell *)[tableView cellForRowAtIndexPath:indexPath];
                [theCell.ivProfile setImage:profileImage];
                [self.facebookUidToImageDownloadOperations removeObjectForKey:friend.uid];
            }
        }];
    }];

    //Save a reference to the operation in an NSMutableDictionary so that it can be cancelled later on
    if (friend.uid) {
        [self.facebookUidToImageDownloadOperations setObject:loadImageIntoCellOp forKey:friend.uid];
    }

    //Add the operation to the designated background queue
    if (loadImageIntoCellOp) {
        [self.imageLoadingOperationQueue addOperation:loadImageIntoCellOp];
    }

    //Make sure cell doesn't contain any traces of data from reuse -
    //This would be a good place to assign a placeholder image
    cell.ivProfile.image = nil;

    return cell;
}

Now all that’s left to do is to take advantage of a new UITableViewDelegate method introduced in iOS 6.0: It’s called “tableView:didEndDisplayingCell:forRowAtIndexPath:” and It’s called right after the cell we are loading our data into is no longer needed. Sounds like a perfect spot for the following code, which fetches the relevant operation and cancels it:

- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
    FacebookFriend *friend = [self.facebookFriends objectAtIndex:indexPath.row];
    //Fetch operation that doesn't need executing anymore
    NSBlockOperation *ongoingDownloadOperation = [self.facebookUidToImageDownloadOperations objectForKey:friend.uid];
    if (ongoingDownloadOperation) {
        //Cancel operation and remove from dictionary
        [ongoingDownloadOperation cancel];
        [self.facebookUidToImageDownloadOperations removeObjectForKey:friend.uid];
    }
}

Also, don’t forget to take advantage of the NSOperationQueue and call “cancelAllOperations” when the table is not needed anymore:

- (void)viewDidDisappear:(BOOL)animated {
    [self.imageLoadingOperationQueue cancelAllOperations];
}

That’s it! You now have yourself a UITableView running as smooth as a Ferrari. You’re welcome 😉

Table_Mountain_DanieVDM

Tabelview, Capetown

Advertisements

Posted on December 14, 2012, in Advanced Issues. Bookmark the permalink. 15 Comments.

  1. Great article! I wanted to give you credit because I used your solution in an open source iPhone app (https://github.com/tonypujals/challenge-ios). Thanks for sharing!

  2. Hi, Great article but I have a strange begaviour when I have scrolled to the last och to the top rows. The whole scroll hangs for just a couple of second, before resuming.. Do you have any ideas on why?

    • Sounds weird… Could you post a more elaborate question on StackOverflow and link to your question? I need to see some code to get an idea.

      • Hi

        I’ve already solved it. Entirely my fault. I did not check if my asynchronous load from
        Facebook API was finished before starting a new load.

  3. Oren Rosenblum

    Hi, very nice tut, but I have a little problem – the operation is not seemed to be cancelled (when I check it in the mainOperationQueue)
    Meaning that weakOp.isCancelled is always NO. If I try to ask about the dictionary (if the operation is inside) it’s ok, because I removed the operation from the dictionary as well.
    Have any idea?

    • Oren Rosenblum

      Well, I kind of find something – after cancel, weakOp is nil, so (!weaOp.isCancelld) is YES..
      Thanks for the tutorial.
      Am I the only one who see this behavior? (P.S, I’m kind of IOS beginner…)

    • Hi Oren.

      Inspecting the dictionary for a pending operation is also a good idea, should work. I’m not sure what the problem you’re describing is about – can you be more specific?

  4. Excellent post. I was using a UICollectionView with SDWebImage. I ended up using “didEndDisplayingCell” method of UICollectionView and ending the current Image download and setting the image to nil. This worked perfectly and there was no more “shuffling” or repetition of images! 🙂

  5. I never got this right before reading your guide. I wasn’t aware of the tableView:didEndDisplayingCell:forRowAtIndexPath: method.

    I’m having an issue though:
    I’m testing on a Cell with a UILabel only. The cells show up empty. The do apear the way they are supposed to when tappin on it.
    Does this make any sense to you?

  6. great post. elegant solution.

  7. Brilliant solution. This was exactly what I needed to get my UITableView scrolling smoothly – I had resize operations and image caching that needed to be done as well. For anyone having this issue using Xamarin, I have ported this code. https://forums.xamarin.com/discussion/31317/asynchonously-load-resize-and-cache-images-without-concurrency-issues-in-uitableview-cells

  8. Thank you very much! It works great.

  1. Pingback: UITableViewCell에 이미지를 비동기적으로 불러올 때의 문제 해결 | Start to Great

Say something...

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: