Woodstock
Woodstock

Reputation: 22926

Reuse of UICollectionViewCells during scrolling

I'm having an issue,

I have a simple UICollectionView with a static 200 cells that load images from Flickr.

my CellForItemAtIndexPath looks like this:

- (UICollectionViewCell *)collectionView:(UICollectionView *)cv cellForItemAtIndexPath:(NSIndexPath *)indexPath {
    UICollectionViewCell *cell = [cv dequeueReusableCellWithReuseIdentifier:@"FlickrCell" forIndexPath:indexPath];
    cell.backgroundColor = [self generateRandomUIColor];

    if(![[cell.subviews objectAtIndex:0] isKindOfClass:[PFImageView class]])
    {
        NSURL *staticPhotoURL = [self.context photoSourceURLFromDictionary:[self.photos objectAtIndex:indexPath.row] size:OFFlickrSmallSize];
        PFImageView *imageView = [[PFImageView alloc] initWithFrame:CGRectMake(0, 0, cell.frame.size.height, cell.frame.size.width) andImageURL:staticPhotoURL andOwningCell:cell];
        [cell addSubview:imageView];
    }
    return cell;
}

PFImageView is a subclass of UIImageView that loads a Flickr photo URL on a background thread and then updates it's own image on the main thread - this works fine.

The logic is really simple - I create a cell if there isn't one dequeueable.

If the cell (which I'm expecting to be dequeued and already have a PFImageView) doesn't have a PFImageView, I alloc and init an imageView for the cell and add it as a subview of the cell.

Thus I expect if the cell has been dequeued it should already have a PFImageView as a subview and as we should not get into the if statement to create a new imageView and kick off a new photo download request

Instead what I see is that the cells at the top and bottom of the UICollectionView that 'go off screen' momentarily - when they come back on screen they are not being reused and seemingly a new cell is created and the picture refreshed.

1) How can I achieve a static image once the cell has been created (i.e. not refreshing when the cell goes slightly off screen.

2) Why are the cells not being reused?

Many thanks for your time.

John

Upvotes: 1

Views: 1551

Answers (4)

Pat
Pat

Reputation: 824

I see several potential issues:

  1. You are looking specifically at index 0 of the cell for the child class that you are adding. The UICollectionViewCell may have other views as children, so you can't just assume that the only (or first) child is the one you added.

  2. I don't see that you are calling registerClass:forCellWithReuseIdentifier: or registerNib:forCellWithReuseIdentifier:, one of which is required for proper use of dequeue (https://developer.apple.com/library/ios/documentation/uikit/reference/UICollectionViewCell_class/Reference/Reference.html).

  3. You are only setting the URL of the PFImageView in the case that you have to construct the PFImageView. The idea with dequeuing reusable views is that you will only construct a small subset of the views needed, and the UITableView will recycle them as they move offscreen. You need to reset the value for the indexPath that is being requested, even when you don't construct the new content.

If your case is as simple as you describe, you can probably get away with adding your PFImageView to the contentView property of your dequeued UICollectionView.

In your controller:

// solve problem 2
[self.collectionView registerClass:[UICollectionViewCell class] forReuseIdentifer:@"FlickrCell"];

In collectionView:cellForItemAtIndexPath

UICollectionViewCell *cell = [cv dequeueReusableCellWithReuseIdentifier:@"FlickrCell" forIndexPath:indexPath];
cell.backgroundColor = [self generateRandomUIColor];

// solve problem 1 by looking in the contentView for your subview (and looping instead of assuming at 0)
PFImageView *pfImageView = nil;
for (UIView *subview in cell.contentView.subviews)
{
    if ([subview isKindOfClass:[PFImageView class]])
    {
        pfImageView = (PFImageView *)subview;
        break;
    }
}

NSURL *staticPhotoURL = [self.context photoSourceURLFromDictionary:[self.photos objectAtIndex:indexPath.row] size:OFFlickrSmallSize];    
if (pfImageView == nil)
{
    // No PFImageView, create one
    // note the use of contentView!
    pfImageView = [[PFImageView alloc] initWithFrame:CGRectMake(0, 0, cell.contentView.frame.size.height, cell.frame.size.width) andImageURL:staticPhotoURL andOwningCell:cell.contentView];
    [cell.contentView addSubview:pfImageView];
}
else
{
    // Already have recycled view.
    // need to reset the url for the pfImageView. (Problem 3)
    // not sure what PFImageView looks like so this is an e.g. I'd probably remove the
    // URL loading from the ctr above and instead have a function that loads the
    // image. Then, you could do this outside of the if, regardless of whether you had 
    // to alloc the child view or not.
    [pfImageView loadImageWithUrl:staticPhotoURL];
    // if you really only have 200 static images, you might consider caching all of them
}
return cell;

For less simple cases (e.g. where I want to visually lay out the cell, or where I have multiple children in the content), I typically customize my UICollectionViewCell's using Interface Builder.

  1. Create a subclass of UICollectionViewCell in the project (In your case, call it PFImageCell).
  2. Add an IBOutlet property to that subclass for the view I want to change in initialization (In your case, a UIImageView). @property (nonatomic, assign) IBOutlet UIImageView *image;
  3. In Interface Builder, create a prototype cell for the UITableView.
  4. In the properties sheet for that prototype cell, identify the UICollectionViewCell subclass as the class.
  5. Give the prototype cell an identifier (the reuse identifier) in the property sheet.
  6. Add the view child in interface builder to the prototype cell (here, a UIImageView).
  7. Use IB to map the IBOutlet property to the added UIImageView
  8. Then, on dequeue in cellForRowAtIndexPath, cast the dequeued result to the subclass (PFImageCell) and set the value of the IBOutlet property instance. Here, you'd load the proper image for your UIImageView.

Upvotes: 1

jbouaziz
jbouaziz

Reputation: 1493

Try adding a tag on this particular UIImageView

- (UICollectionViewCell *)collectionView:(UICollectionView *)cv cellForItemAtIndexPath:(NSIndexPath *)indexPath {

    static int photoViewTag = 54353532;

    UICollectionViewCell *cell = [cv dequeueReusableCellWithReuseIdentifier:@"FlickrCell" forIndexPath:indexPath];
    cell.backgroundColor = [self generateRandomUIColor];

    PFImageView *photoView = [cell.contentView viewWithTag:photoViewTag];

    // Create a view
    //
    if (!photoView) {
        photoView = [[PFImageView alloc] initWithFrame:CGRectMake(0, 0, cell.frame.size.height, cell.frame.size.width) andImageURL:staticPhotoURL andOwningCell:cell];
        imageView.tag = photoViewTag;
        [cell.contentView addSubview:imageView];
    }

    // Update the current view
    //
    else {
        NSURL *staticPhotoURL = [self.context photoSourceURLFromDictionary:[self.photos objectAtIndex:indexPath.row] size:OFFlickrSmallSize];
        photoView.imageURL = staticPhotoURL;
    }
    return cell;
}

I would really recommend to create your own UICollectionViewCell subclass though.

EDIT: Also, note that I used the contentView property instead of adding it directly to the cell.

Upvotes: 1

Tommy
Tommy

Reputation: 100622

UICollectionView will reuse cells for maximum efficiency. It does not guarantee any particular reuse or population strategies. Anecdotally, it seems to place and remove cells based on integer power of two regions — e.g. on a non-retina iPad it might divide your scroll area up into regions of 1024x1024 and then populate and depopulate each of those regions as they transition into and out of the visible area. However you should not predicate any expectations on its exact behaviour.

In addition, your use of collection view cells is incorrect. See the documentation. A cell explicitly has at least two subviews — backgroundView and contentView. So if you add a subview it will be at index 2 at the absolute least and, in reality, the index will be undefined. In any case you should add subviews to contentView, not to the cell itself.

The most normal way of doing what you're doing would be to create a custom UICollectionView subclass that inherently has a PFImageView within it.

Upvotes: 2

parram
parram

Reputation: 309

I am not sure if the cell is being re-used or not. It may be being reused but the subview may not be there. My suggestion would be to create a PFImageViewCollectionViewCell Class (sub class of UICollectionViewCell) and register it as the CollectionView Cell and try. That's how I do and would do if I need a subview inside a cell.

Upvotes: 1

Related Questions