I have a UITableView
that loads images from a URL into cells asynchronously using GCD. Problem is if a user flicks past 150 rows, 150 operations queue up and execute. What I want is to dequeue/cancel the ones that blew past and went off screen.
How do I do this?
My code at this point (pretty standard):
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
// after getting the cell...
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
if (runQ) {
NSString *galleryTinyImageUrl = [[self.smapi getImageUrls:imageId imageKey:imageKey] objectForKey:@"TinyURL"];
NSData *imageData = [[NSData alloc] initWithContentsOfURL:[NSURL URLWithString:galleryTinyImageUrl]];
dispatch_async(dispatch_get_main_queue(), ^{
if (imageData != nil) {
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
cell.imageView.image = [UIImage imageWithData:imageData];
}
});
}
});
runQ is a BOOL
ivar I set to NO
on viewWillDisappear
, which (I think) has the effect of flushing out the queue rapidly when this UITableView
pops off the navigation controller.
So, back to my original question: how do I cancel the image fetch operations for cells that have gone off screen? Thanks.
First, don't queue operations while scrolling. Instead, load images for just the visible rows in viewDidLoad
and when the user stops scrolling:
-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
for (NSIndexPath *indexPath in [self.tableView indexPathsForVisibleRows]) {
[self loadImageForCellAtPath:indexPath];
}
}
If you still want to be able to cancel loading for invisible cells, you could use NSBlockOperation
instead of GCD:
self.operationQueue = [[[NSOperationQueue alloc] init] autorelease];
[self.operationQueue setMaxConcurrentOperationCount:NSOperationQueueDefaultMaxConcurrentOperationCount];
// ...
-(void)loadImageForCellAtPath:(NSIndexPath *)indexPath {
__block NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
if (![operation isCancelled]) {
NSString *galleryTinyImageUrl = [[self.smapi getImageUrls:imageId imageKey:imageKey] objectForKey:@"TinyURL"];
NSData *imageData = [[NSData alloc] initWithContentsOfURL:[NSURL URLWithString:galleryTinyImageUrl]];
dispatch_async(dispatch_get_main_queue(), ^{
if (imageData != nil) {
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
cell.imageView.image = [UIImage imageWithData:imageData];
}
});
}
}];
NSValue *nonRetainedOperation = [NSValue valueWithNonretainedObjectValue:operation];
[self.operations addObject:nonRetainedOperation forKey:indexPath];
[self.operationQueue addOperation:operation];
}
Here operations
is an NSMutableDictionary
. When you want to cancel an operation, you retrieve it by the cell's indexPath
, cancel it, and remove it from the dictionary:
NSValue *operationHolder = [self.operations objectForKey:indexPath];
NSOperation *operation = [operationHolder nonretainedObjectValue];
[operation cancel];
You cannot cancel GCD operations after they've been dispatched. You can however check in the dispatch blocks to see if the code needs to continue on or not. In -tableView:cellForRowAtIndexPath: you can do something like this
UIImage *image = [imageCache objectForKey:imageName];
if(image) {
cell.imageView.image = image;
else {
dispatch_async(globalQueue, ^{
//get your image here...
UIImage *image = ...
dispatch_async(dispatch_get_main_queue(), ^{
if([cell.indexPath isEqual:indexPath){
cell.indexPath = nil;
cell.imageView.image = image;
[cell setNeedsLayout];
}
});
[imageCache setObject:image forKey:imageName];
});
}
Basically use a image cache (NSMutableDictionary) and try to grab the image. If you don't have it then dispatch a block and get it. Then dispatch back to the main thread, check the index path and then set the image and finally cache the image. This example uses a custom tableview cell that has the index path stored with it as a property.
So its a little extra work, to make this work well, but its worth it.
If you are using dequeued cells the best approach is to cancel GCD in
-[UITableViewCell prepareForReuse]