The right way to load content in background thread with TableView
UITableView is extremely useful class to list content which Apple provide to iOS developer. UITableView use “reusable” pattern to handle content in each cell. So it can contain 100+ row with good performance.
There are many common scenarios when you deal with UITableView.
One of this is load asynchronous content in cell.
Like this
-(UITableViewCell *) tableView:(UITableView *) tableView cellForRowAtIndexPath:(NSIndexPath *) indexPath
{
MyCustomeCell *cell = (MyCustomeCell *)[tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];
// weak cell
__weak MyCustomeCell *weakCell = cell;
// Async
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
// Get Image
NSURL *url = // URL link
NSData *data = [NSData dataWithContentsOfURL:url];
UIImage *image = [UIImage alloc] initWithData:data];
// Update UI
dispatch_async(dispatch_get_main_queue(), ^{
weakCell.imageView = image;
});
});
}
But, in this code have some issues memory.
Image you have 100 row, each of row you must load async big UIImage. So before cell is being displayed. TableView will call “Data Source” which provide cell. And in cellForRowAtIndexPath: will fire async method to load content.
But if this cell is OUT of visible area. The async operation you called is still doing work. So when this operation update UI in main thread, this UIImage is unnecessary resource. Sometime, it cause some “buggy”.
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.
So, to handle this common scenarios. I use NSOperation and NSOperationQueue Apple was introduced NSOperationQueue and many relative class in iOS 4. This was build in top of Grand Dispatch Central. But improve more enhancement. NSOperation can be cancel and resume easily.
In brieft, we will implement step-by-step :
- Init array or dictionary
- In cellForRowAtIndexPath:, we will create NSOperationBlock and add to background thread. You must implement how it do if this operation is NOT cancel. Finally, we add to array or dictionary to usable.
- Implement didEndDisplayingCell, this method will be called if cell is OUT of visible area.
- Get operation in arr / dictionary and cancel. And remove out.
Here is sample :
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
MyCustomeCell *cell = (MyCustomeCell *)[tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];
// Get URL
NSURL *url = _arrURL[indexPath.row];
//Create a block operation for loading the image into the profile image view
NSBlockOperation *loadImageOperation = [[NSBlockOperation alloc] init];
//Define weak operation so that operation can be referenced from within the block without creating a retain cycle
__weak NSBlockOperation *weakOperation = loadImageOperation;
[loadImageOperation addExecutionBlock:^(void){
//Some asynchronous work. Once the image is ready, it will load into view on the main queue
UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:url]];
// Update UI in main thread when completion
[[NSOperationQueue mainQueue] addOperationWithBlock:^(void) {
//Check for cancelation before proceeding. We use cellForRowAtIndexPath to make sure we get nil for a non-visible cell
if (! weakOperation.isCancelled)
{
MyCustomeCell *theCell = (MyCustomeCell *)[tableView cellForRowAtIndexPath:indexPath];
theCell.imageView.image = image;
// Remove operation
[_myDictionary removeObjectForKey:indexPath];
}
}];
}];
//Save a reference to the operation in an NSMutableDictionary so that it can be cancelled later on
if (url)
{
[_myDictionary setObject: loadImageOperation forKey:indexPath];
}
//Add the operation to the designated background queue
if (loadImageOperation) {
[_operationQueue loadImageOperation];
}
//This would be a good place to assign a placeholder image
cell.imageView.image = placeholdImage;
return cell;
}
and we take advantage in didEndDisplayngCell method.
We will cancel operation which called in cellForRowAtIndexPath: before.
- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
//Fetch operation that doesn't need executing anymore
NSBlockOperation *ongoingDownloadOperation = [_myDictionary objectForKey:indexPath];
// Ensure ongoingDownloadOperation is not nil
if (ongoingDownloadOperation)
{
//Cancel operation and remove from dictionary
[ongoingDownloadOperation cancel];
[_myDictionary removeObjectForKey:indexPath];
}
}
So, this is the best way to handle this scenarios. You can cancel all of operation if you push to new UIViewController.
-(void) viewWillDisappeard:(BOOL) animated
{
[super viewWillDisappeard:animated];
// Cancel all
[_myOperationQueue cancelAllOpeations];
}
Finally, you can improve more performance by setting setMaxConcurrentOperationCount
.
[_myOpeationQueue setMaxConcurrentOperationCount:3];
Feel free for giving me your comment or your opinion.
Thanks for reading ;]