0

In my Core Data model I have an entity which keeps URLs of images. I want to download these images so I can use them in my UICollectionView model, which is an array of my Core Data entities. I want to always be able to access these images synchronously (assuming they have already been downloaded async) so there's no delay between them loading in their respective cell.

Currently, I am using an async method in the cellForIndexPath: data source delegate method of the UICollectionView but when the collection view is reloaded, if there are images still being downloaded, they get assigned to the wrong cell as cells have been inserted during this time.

This problem seems like it should be very obvious to solve but I cannot work it out.

Any ideas are greatly appreciated.

Thanks.

Adam Carter
  • 4,741
  • 5
  • 42
  • 103
  • Check this out. It is a UITableView but I think it still applies: http://stackoverflow.com/questions/7852033/gcd-uitableview-asynchronous-load-images-wrong-cells-are-loaded-until-new-image?lq=1 They suggest you use the NSIndexPath instead of capturing the cell in the asynchronous block. – The dude Apr 07 '14 at 15:58
  • Here suggest a different solution based on UIView's tag property: http://stackoverflow.com/a/15668366/1152596 – The dude Apr 07 '14 at 16:04

2 Answers2

1

You can download the images asynchronously in viewDidLoad, and add them to an NSMUtableArray as follows

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

        for (int i = 0; i < carPhotos.count; i++) {

            NSURL *photoURL = [NSURL URLWithString:carPhotos[i]];
            NSData *photoData = [NSData dataWithContentsOfURL:photoURL];
            UIImage *image = [UIImage imageWithData:photoData];
            [carImages addObject:image];

            dispatch_async(dispatch_get_main_queue(), ^{
                [self.tableView reloadData];
            });

        }

    });

And then check in the cellForRowAtIndexpath to ensure that the indexPath matches the arrayIndex, and if you want load a dummy image for not-loaded images, as follows:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath

{ static NSString *CellIdentifier = @"Cell"; CarsAppCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath]; cell.carMakeLabel.text = carMakes[indexPath.row]; cell.carModelLabel.text = carModels[indexPath.row];

if (carImages.count > indexPath.row) {
    cell.carImage.image = carImages[indexPath.row];
} else {
    cell.carImage.image = [UIImage imageNamed:@"dummy.png"];
}

return cell;

}

I hope it helps....

eharo2
  • 2,553
  • 1
  • 29
  • 39
  • This was what I originally thought about doing, but my UICollectionView uses data from the core data as well as the loaded image, so there's a danger of the two datas being out of sync. – Adam Carter Apr 07 '14 at 16:00
0

A couple of things you can do:

  1. In the cell's prepareForReuse you can cancel any (still) pending image request.
  2. In the completion process for the image request you can verify that the image url is still as expected before setting the image.

Something like:

-(void)prepareForReuse
{
    [super prepareForReuse];
    _imageUrl = nil;
}

-(void)useImageUrl:(NSString*)imageUrl 
{
    _imageUrl = imageUrl;
    if(_imageUrl)
    {
        __weak typeof(self) weak = self;

        [UIImage fetchRemoteImage:imageUrl completion:^(UIImage* image){
            if(weak.imageUrl && [weak.imageUrl isEqualToString:imageUrl] && image) {
                weak.imageView.image = image;
            }
        }];
    }
}
David Berry
  • 40,941
  • 12
  • 84
  • 95
  • Maybe I'm confused, but how does the first method cancel the request? The second method is a nice idea though :) – Adam Carter Apr 07 '14 at 15:58
  • This implementation doesn't implement #1, so you're not confused. – David Berry Apr 07 '14 at 16:00
  • It just uses prepareForReuse to add a little protection against the image begin set wrong. – David Berry Apr 07 '14 at 16:01
  • Ah ok, exactly why is weak needed? I remember using it before but forget why. – Adam Carter Apr 07 '14 at 16:11
  • In this case it's not strictly "needed" since there is no retain cycle (the cell isn't retaining a reference to the block somehow) However, it will allow the cell to be safely released before the network operation completes. – David Berry Apr 07 '14 at 16:13
  • Is there possibly a way to use NSURLSession or similar to cancel the download request? – Adam Carter Apr 07 '14 at 16:22
  • Yeah, that depends on how you're actually doing the operation though. Most of the network toolkits have some mechanism for cancellation. You probably want to think about whether or not it's really a good idea though. How likely are they to go back to a cell they've scrolled off screen? How likely are the images to be shared between multiple cells? etc. – David Berry Apr 07 '14 at 16:38
  • It appears that my issue has been solved through reassessing how and when my code gets new data (i.e. to insert new cells) as well as setting the image view's image to nil in `prepareForReuse` – Adam Carter Apr 07 '14 at 16:41
  • I can't imagine that this is an absolute fix but if any problems happen again I will have to post all of my code – Adam Carter Apr 07 '14 at 16:42