UICollectionView prefetching can cause display of pages hidden for reuse
Originator: | matej | ||
Number: | rdar://39604024 | Date Originated: | 20-Apr-2018 08:48 PM |
Status: | Duplicate of 34932237 | Resolved: | |
Product: | iOS + SDK | Product Version: | iOS 10 and 11 |
Classification: | Serious Bug | Reproducible: | Always |
Summary: In the PSPDFKit framework we use a collection view with custom layouts as the main component for document navigation. After removing our own prefetching code and switching to native UICollectionView prefetching, we noticed that when invoking some specific navigation steps and triggering `invalidateLayout` on the collection view layout, we could end up in a state where cells with `_isHiddenForReuse` set to `YES` were displayed in the visible area of the collection view. Steps to Reproduce: I'm attaching a GIF illustrating this behavior. Showing or hiding the navigation bar causes an `invalidateLayout` call on iOS 11, due to changes in the `safeAreaInsets`. That's why we first observed the issue on iOS 11. The same issue occurs on iOS 10, if we trigger `invalidateLayout` manually. I tried reproducing the same problem in an isolated sample, but was unable to do so. I'm however providing some more detailed findings during my debugging session that lead me to believe that this is not an issue on our side. Expected Results: Visible cells would always be visible `_isHiddenForReuse = NO`. Actual Results: With prefetching enabled, cells can become part of the `visibleCells` list with `_isHiddenForReuse = YES`. Those cells do not show up on screen. Even in the view debugger. They can however be accessed via the `visibleCells` property. To trigger this behavior the prefetching state (prefetched cells) need to be in specific configurations and a `invalidateLayout` call needs to be triggered. Version: iOS 10 and 11 Notes: See DebuggingNotes.txt for a detailed description of the problematic state observed in UIKit.
Comments
Please note: Reports posted here will not necessarily be seen by Apple. All problems should be submitted at bugreport.apple.com before they are posted here. Please only post information for Radars that you have filed yourself, and please do not include Apple confidential information in your posts. Thank you!
After further debugging this turned turned out that the issue is triggered by a subview of our disappearing cell having first responder status. Our updated workaround is to call
resignFirstResponder
before the cell moves ofscreen.i can't find good method
cell.layer.opaque = true; cell.layer.hidden = false;
Behavior
https://cloudup.com/cBIZsBlBlv9
DebuggingNotes.txt
With prefetching enabled
UICollectionView
uses a cache for pages that are loaded, but not yet displayed on screen. This is a dictionary<NSIndexPath : _UICollectionViewPrefetchItem>
, stored under the_prefetchCacheItems
instance variable._UICollectionViewPrefetchItem
stores the layout attributes and reusable view (UICollectionViewCell
in our case) for a specific index path. Views that are put into_prefetchCacheItems
also get their visibility set to "hidden". This is however not via the default hidden property, bit via dedicated methods_isHiddenForReuse
and_setHiddenForReuse:
defined onUIView
.hidden
reflects the value of_isHiddenForReuse
, but does not change it. When prefetched cells are determined to become visible, they are removed from the cache and_isHiddenForReuse
is set toNO
so they show up.If the collection view layout gets invalidated (which happens for us when we show or hide the navigation bar), the prefetch cache gets cleared. During this, new layout attributes and potentially new prefetched cells might get queried from the layout / data source. Those cells might not match the previous prefetched cells (e.g., only cells in the last scrolled direction are preloaded again).
Here's where the problem appears to happen. During cache eviction, the removed cells do not get their
_isHiddenForReuse
value updated. It remains the same. Some of those cells then get re-added to new_UICollectionViewPrefetchItem
objects, which ensures their_isHiddenForReuse
is updated when the cell comes on screen. But some cells don't get "prefetched" again. There is no_UICollectionViewPrefetchItem
created for them. Their_isHiddenForReuse
value remains set and they remain at the same position they were before invalidating the layout.UICollectionView
appears to be perfectly ok with thinking there's nothing wrong with that cell and just keeps it in position as is. It even sends out "will appear" delegate calls for it. But since the cell is hidden, we can't see anything.I verified that manually ensuring
_isHiddenForReuse
is not set for every visible cell during layout passes fixes the issues. Prefetching appears to work fine after that.