3

I have an app that been happily shipping for a couple of years. It retrieves RSS Feeds in a UICollectionView. The cellForItemAtIndexPath method sets text and calls a datasource method to load an image from a link specified in the feed. If none exists it loads the web page data and searches for <img> tags to get an image. Once the image is found/loaded the delegate method is called to add the image to the cell. (below)

When running in iOS 8 and 9 everything is happy, but when running in iOS 10 the visible cells are updated with images when the RSS feed is initially loaded but when scrolling no images are added and I get NULL from cellForItemAtIndexPath.

The image is displayed when I scroll back and the image is displayed if I add a reloadItemsAtIndexPaths to imageWasLoadedForStory but reloadItemsAtIndexPaths destroys performance.

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

          if (story.thumbnail) {
          [imageView setAlpha: 1.0];
          imageView.image = story.thumbnail;
          UIActivityIndicatorView *lActivity = (UIActivityIndicatorView *) [collectionView viewWithTag: 900];
          [lActivity removeFromSuperview];
          }else{
               [story setDelegate: self];
               [story findArticleImageForIndexPath: indexPath];
          }
          //  ...
}



//delegate method.  Called when image has been loaded for cell at specified indexpath

- (void) imageWasLoadedForStory: (RSSStory *) story forIndexPath: (NSIndexPath *) indexPath
{
        //get cell
        CustomCollectionViewCell *customCell = (id) [self.collectionview cellForItemAtIndexPath: indexPath];

        NSLog(@"imageWasLoadedForStory row %i section %i  and class %@", (int)indexPath.row, (int)indexPath.section, [customCell class]);

        //if cell is visible ie: cell is not nil then update imageview
        if (customCell) {
                 UIImageView *imageView = (UIImageView *) [customCell viewWithTag: 300];
                 imageView.image = story.thumbnail;
                 UIActivityIndicatorView *lActivity = (UIActivityIndicatorView *) [customCell viewWithTag: 900];
                 [lActivity removeFromSuperview];
                 [customCell setNeedsLayout];
                 [customCell setNeedsDisplay];
                 }
                    //[self.collectionview reloadItemsAtIndexPaths: [NSArray arrayWithObject: indexPath]];           
}



- (void) findArticleImageForIndexPath: (NSIndexPath *) indexPath
{
           //kick off image search
           dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

                self.thumbnail = [self findArticleForStoryForIndexPath:indexPath];
                dispatch_async( dispatch_get_main_queue(), ^{
                //image set - return
                      [self.delegate imageWasLoadedForStory: self forIndexPath: indexPath];
                });
        });
}
Mr Lister
  • 45,515
  • 15
  • 108
  • 150
Joe Fratianni
  • 790
  • 8
  • 24
  • Well, I may not be crazy. I have been able to get it to work by using the tag property. In cellForItemAtIndexPath I added: [cell setTag: indexPath.row]; And in my delegate method added: CustomCollectionViewCell *customCell = (CustomCollectionViewCell *) [self.collectionview viewWithTag: indexPath.row]; Which makes everything work but I don’t think this is a regulation way to handle this. – Joe Fratianni Oct 30 '16 at 02:59

3 Answers3

22

I encourage everyone to read up on Prefetching - new in iOS 10. The simple solution is this:

[self.customCollectionview setPrefetchingEnabled:NO];

As it turns out cells are now prefetched. This means there is now a difference between loaded/nonvisible cells and loaded/visible cells.

In iOS 10 a cell can now be preloaded but it will still return nil if it's not visible. So cellForItemAtIndexPath is called to preload a cell. It is then entirely possible the image will finish loading and cellForItemAtIndexPath will return nil if the cell is not visible. That means the imageView will not be set. When scrolling the image will not be added to the cell since the cell was already created.

Getting loaded vs visible cells on a UITableView or UICollectionView

Community
  • 1
  • 1
Joe Fratianni
  • 790
  • 8
  • 24
  • Thank you very much Joe :) . – Nada Gamal Jan 04 '17 at 03:24
  • 1
    unfortunately your solution is not working in my case. I set `self.collectionView.isPrefetchingEnabled = false` in my `viewDidLoad()` but when I call `cellForItem(at:)` in `didDeselectItemAt indexPath:` I still get `nil` returned. – Marcel T Mar 20 '17 at 09:58
  • There must be something else going on with what you are describing. (i.e. wrong indexPath, etc) If someone is selecting a cell then it must be loaded. The only time prefetching applies is BEFORE cells are displayed. i.e. a user is scrolling down and cells 2 through 8 are visible. With iOS 10 cells 9 through 15 are preloaded in anticipation of becoming visible. That is the only time prefetching is an issue. – Joe Fratianni Mar 20 '17 at 14:21
  • Oh man. For a few hours I thought I was having a threading issue and that's why things weren't loading correctly. Thanks! – Sami Jul 09 '18 at 04:07
1

As per Joe Fratianni's answer this is due to prefetching, which can be disabled. Of course, that also loses the benefits of prefetching.

The approach recommended by Apple in the documentation for collectionView(_:cellForItemAt:) is to instead update cell appearance in collectionView(_:willDisplay:forItemAt:).

That way, the cell is either visible and available from cellForItem(at:) for ad-hoc updates, or is not visible but gets up-to-date information when it scrolls into view.


Another alternative that worked for me with a simpler code change while retaining some benefit of prefetching was to wrap the updates with calls to cellForItem(at indexPath) within a performBatchUpdates block, e.g.:

class MyViewController: UIViewController {
    @IBOutlet weak var collectionView: UICollectionView!
    ...
    func someMethod() {
        ...
        collectionView.performBatchUpdates {
            ...
            if let cell = collectionView.cellForItem(at: indexPath) as? MyCell {
                // Update cell, e.g.:
                cell.someField = someValue
            }
            ...
        } completion: { _ in }
        ...
    }
    ...
}

Apparently the performBatchUpdates call discards any cells that have been prepared in collectionView(_:cellForItemAt:) but are not yet visible (due to prefetch).

Unfortunately, it seems those discarded cells aren’t prefetched again, and will be reloaded only once they become visible. So some benefit of prefetching is lost.

jedwidz
  • 384
  • 5
  • 7
0

Make sure cell you're trying to access with [self.collectionview cellForItemAtIndexPath: indexPath] is visible.

It will always return nil if it is not visible.

You should access your datasource (array, core data, etc.) to get data you're showing in that cell instead of accessing cell itself.

Aleksandras
  • 595
  • 6
  • 6
  • Thank you. I expect nil if not visible but what is bizarre is that it works perfectly if I run in iOS 9. Additionally it works perfectly in iOS 9 & iOS 10 if I instead add a tag property equal to the cells' row and retrieve the cell using: CustomCollectionViewCell *customCell = (CustomCollectionViewCell *) [self.collectionview viewWithTag: indexPath.row]; – Joe Fratianni Oct 31 '16 at 23:38
  • Frustrating. Returns nil if I scroll slowly. i.e. cell is created but not yet visible when image is loaded. If I scroll fast to a cell then it works i.e. loaded image is returned and the cell is visible. – Joe Fratianni Nov 02 '16 at 02:07