Joe Fratianni Joe Fratianni - 9 months ago 139
iOS Question

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

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 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];
[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];

Answer Source

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