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?
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.
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;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With