Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In a UITableView, best method to cancel GCD operations for cells that have gone off screen?

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.

like image 753
Steven Avatar asked May 02 '12 06:05

Steven


1 Answers

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];
like image 200
Christopher Pickslay Avatar answered Nov 20 '22 12:11

Christopher Pickslay