`
dadi520
  • 浏览: 139178 次
  • 性别: Icon_minigender_1
  • 来自: 重庆
社区版块
存档分类
最新评论

Asynchronous UITableViewCell content loading done right

    博客分类:
  • ios
 
阅读更多

http://stavash.wordpress.com/2012/12/14/advanced-issues-asynchronous-uitableviewcell-content-loading-done-right/

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

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics