Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Reusing UITableViewCell with GCD

I'm using Grand Central Dispatch to load images of a UITableViewCell asynchronously. This works well except in some border cases in which the cell is reused, and a previous block loads the wrong image.

My current code looks like this:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *CellIdentifier = @"Cell";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (!cell) {
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
    }

    NSString *imagePath = [self imagePathForIndexPath:indexPath];         
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);

    dispatch_async(queue, ^{
        UIImage *image = [UIImage imageWithContentsOfFile:imagePath];

        dispatch_sync(dispatch_get_main_queue(), ^{
            cell.imageView.image = image;
            [cell setNeedsLayout];
        });
    });

    return cell;
}

As far as I know GCD queues cannot be stopped. How can this border case be prevented then? Or should I be using something else instead of GCD to solve this problem?

like image 693
hpique Avatar asked Oct 07 '22 22:10

hpique


2 Answers

what i've done is added an NSOperation ivar to the cell. the operation is responsible for getting, loading, and creating the image. when completed, it pings the cell. when/if the cell is dequeued, the operation is cancelled and destroyed if it has not finished. the operation test for cancellation when in -main. after the image is passed to the cell, the cell drops the operation and uses the image.

like image 163
justin Avatar answered Oct 13 '22 22:10

justin


You may try to force a reset of your image before you start your async request.

As user scroll the table, he may see the old image before your async method changes it with the right one

just add this line:

cell.imageView.image = nil; // or a placeHolder image
dispatch_async(queue, ^{
    UIImage *image = [UIImage imageWithContentsOfFile:imagePath];

    dispatch_sync(dispatch_get_main_queue(), ^{
        cell.imageView.image = image;
        [cell setNeedsLayout];
    });
});

EDIT:

@hgpc:

I don't think this solves the problem. A previous block can still set the wrong image before a new block. – hgpc 9 mins ago

You're right, if user scroll fast this may be a problem...

the only way i see is to make a custom cell with a counter property where to store (in a sync way) the number of operationn in queue of each cell, and then in the async method check that the counter is == 1 before to change the image:

    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
        static NSString *CellIdentifier = @"Cell";

        UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
        if (!cell) {
    // MyCustomUITableViewCell has a property counter
            cell = [[[MyCustomUITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
            cell.counter = 0;
        }

        NSString *imagePath = [self imagePathForIndexPath:indexPath];         
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);

        cell.imageView.image = nil; // or a placeHolder image
        cell.counter = cell.counter + 1;
        dispatch_async(queue, ^{
            UIImage *image = [UIImage imageWithContentsOfFile:imagePath];

            dispatch_sync(dispatch_get_main_queue(), ^{
                if (cell.counter == 1){
                    cell.imageView.image = image;
                   [cell setNeedsLayout];
                }
                cell.counter = cell.counter - 1;

            });
        });

    return cell;
}
like image 21
meronix Avatar answered Oct 13 '22 22:10

meronix