Asynchronous downloading of images for UITableView with GCD
Solution 1:
The problem here is that your image-fetching blocks are holding references to the tableview cells. When the download completes, it sets the imageView.image
property, even if you have recycled the cell to display a different row.
You'll need your download completion block to test whether the image is still relevant to the cell before setting the image.
It's also worth noting that you're not storing the images anywhere other than in the cell, so you'll be downloading them again each time you scroll a row onscreen. You probably want to cache them somewhere and look for locally cached images before starting a download.
Edit: here's a simple way to test, using the cell's tag
property:
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = @"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
cell.tag = indexPath.row;
NSDictionary *parsedData = self.loader.parsedData[indexPath.row];
if (parsedData)
{
cell.imageView.image = nil;
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^(void) {
NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:parsedData[@"imageLR"]];
UIImage* image = [[UIImage alloc] initWithData:imageData];
if (image) {
dispatch_async(dispatch_get_main_queue(), ^{
if (cell.tag == indexPath.row) {
cell.imageView.image = image;
[cell setNeedsLayout];
}
});
}
});
cell.textLabel.text = parsedData[@"id"];
}
return cell;
}
Solution 2:
The point is that you did not fully understand the cell reusing concept. That does not agree very well with asynchronous downloads.
The block
^{
cell.imageView.image = image;
[cell setNeedsLayout];
}
gets executed when the request is finished and all data was loaded. But cell gets its value when the block is created.
By the time when the block is executed cell still points to one of the existing cells. But it is quite likely that the user continued scrolling. The cell object was re-used in the meantime and the image is associated with an 'old ' cell that is reused and assigned and displayed. Shortly after that the correct image is loaded and assigned and displayed unless the user has scrolled further. and so on and so on.
You should look for a smarter way of doing that. There are lots of turorials. Google for lazy image loading.
Solution 3:
Use the index path to get the cell. If it's not visible the cell will be nil
and you won't have an issue. Of course you'll probably want to cache the data when it's downloaded so as to set the cell's image immediately when you have the image already.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = @"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
if (self.loader.parsedData[indexPath.row] != nil)
{
cell.imageView.image = nil;
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^(void) {
// You may want to cache this explicitly instead of reloading every time.
NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:[self.loader.parsedData[indexPath.row] objectForKey:@"imageLR"]]];
UIImage* image = [[UIImage alloc] initWithData:imageData];
dispatch_async(dispatch_get_main_queue(), ^{
// Capture the indexPath variable, not the cell variable, and use that
UITableViewCell *blockCell = [tableView cellForRowAtIndexPath:indexPath];
blockCell.imageView.image = image;
[blockCell setNeedsLayout];
});
});
cell.textLabel.text = [self.loader.parsedData[indexPath.row] objectForKey:@"id"];
}
return cell;
}