UIcollectionView cellForItemAtIndexPath returns Null in iOS 10 only. Works fine in iOS 9 and iOS 8
Asked Answered
L

3

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];
                });
        });
}
Laughry answered 29/10, 2016 at 19:43 Comment(1)
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.Laughry
L
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

Laughry answered 2/11, 2016 at 18:26 Comment(4)
Thank you very much Joe :) .Ruano
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.Adrastus
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.Laughry
Oh man. For a few hours I thought I was having a threading issue and that's why things weren't loading correctly. Thanks!Heighho
H
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.

Hearthstone answered 4/1, 2021 at 17:3 Comment(0)
C
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.

Cellarage answered 31/10, 2016 at 6:29 Comment(2)
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];Laughry
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.Laughry

© 2022 - 2024 — McMap. All rights reserved.