简体   繁体   中英

GCD and cell reuse in UICollectionView

I have a UICollectionView implementation with a custom UICollectionViewCell . The array of data is fetched from the internet but the images are downloaded as the user is accessing the content.

While this is probably not the most efficient way and I do not plan on leave it this way, it did expose a certain issue which I am struggling to solve. It seems that the cells are displaying "cached" or "old" images and as I slowly scroll the collection view, the cell images keep on changing.

This is probably an issue with the actual cell image not being available from the net at that moment in time and my question is how do I force an empty cell or some loading activitity monitor and not displaying an incorrect image?

Thanks in advance, pinion

-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
static NSString * CellIdentifier = @"Event";

EventCollectionViewCell *cell  = [collectionView dequeueReusableCellWithReuseIdentifier:CellIdentifier forIndexPath:indexPath];


NSString *title = [[events objectAtIndex:indexPath.item] objectForKey:@"title"];
NSString *imageUrl = [[[events objectAtIndex:indexPath.item] objectForKey:@"photo"] objectForKey:@"url"];


dispatch_queue_t imageFetchQ = dispatch_queue_create("image fetched", NULL);
dispatch_async(imageFetchQ, ^{

    NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:imageUrl]];

    if (imageData)
    {

        dispatch_async(dispatch_get_main_queue(), ^{

            cell.eventImage.image = [UIImage imageWithData:imageData];
            cell.eventTitle.text = title;

        });
    }
});


return cell;

}

This is a common problem. The big issue is that by the time the cell's image arrives, the cell may have been recycled and may be used for some other item. If you try to set the image for the cell she the image is finally available, there's a good chance that you'll be setting the image for the wrong item.

So, don't try to update the cell directly. Don't even keep a reference to the cell in your image downloading completion block. Instead, have the completion block update the data model, whatever that looks like. You can also keep a weak reference to the collection view, and store the cell's index path. When the image arrives, the completion code can call -reloadItemsAtIndexPaths: , which will cause the collection view to reload the cell for the affected index path if the cell is still visible. Your -collectionView:cellForItemAtIndexPath: method just does its usual thing, but this time the image will be available in the data model.

So, your code will look something like:

-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString * CellIdentifier = @"Event";
    EventCollectionViewCell *cell  = [collectionView dequeueReusableCellWithReuseIdentifier:CellIdentifier forIndexPath:indexPath];

    Event *event = [events objectAtIndex:indexPath.item];  // replace "Event" with whatever class you use for your items
    cell.eventTitle.text = [event objectForKey:@"title"];
    cell.eventImage.image = [event objectForKey:@"image"];
    if (cell.eventImage.image == nil) {
        NSString *imageUrl = [[[events objectAtIndex:indexPath.item] objectForKey:@"photo"] objectForKey:@"url"];

        dispatch_queue_t imageFetchQ = dispatch_queue_create("image fetched", NULL);
        dispatch_async(imageFetchQ, ^{
            __weak UICollectionView *weakCollection;
            NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:imageUrl]];
            UIImage *image = [UIImage imageWithData:imageData];
            if (image)
            {
                dispatch_async(dispatch_get_main_queue(), ^{
                    [event setObject:image forKey:@"image"]; // updating the model here
                    [weakCollection reloadItemsAtIndexPaths:@[indexPath]];
                });
            }
        });
    }
    return cell;
}

I haven't tested this exact code since I don't know what your data model looks like. Be sure to substitute your own data item class for the Event class that I assumed. However, I use the same technique in my own code, and I think it's the right way to go. The highlights:

  • Updating the image in your data model means that it will be available the next time the item needs to be displayed, even if the item isn't visible when the image is downloaded.
  • Using a weak reference to the collection view avoids problems if images are still arriving when the user navigates away from the collection view.
  • Calling -reloadItemsAtIndexPaths: helps to limit changes to the cell to your ...cellForItemAtIndexPath: method.

One caveat is that if the list of things you're displaying can change (eg if the user can add or delete items over time), then it may not even be safe for your completion code to rely on the index path staying the same. In that case, you'll need to determine the index path for the item when the image arrives.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM