I think I've just found a bug in UICollectionView. Specifically in how it informs you about the currently visible cells.
Here's the bug report I just submitted to Apple (and Open Radar):
<bug report>
Summary:
UICollectionView's visibleCells and indexPathsForVisibleItems return arrays of items that are in the wrong order.
I have a UICollectionView using the default layout that is configured in my Storyboard to display a single row of items scrolling horizontally. I have implemented the UICollectionView's scrollViewDidScroll delegate method. In my implementation of this method I want to determine the 'first visible item' in order to display some information about this item in another part of my app.
To determine the 'first visible item' I ask the collection view for it's visibleCells or the indexPathsForVisibleItems. Given these methods return an ordered array and that the documentation doesn't state otherwise, I'd expect the order of the items in these arrays to match the order of the cells in my collection view. However, in both cases the order of the items that are returned do not always match the order of those items within the collection view.
Steps to Reproduce:
- Create a UICollectionView with some items.
- In the collection view's delegate implement the scrollViewDidScroll method.
- Ask the collection view for its visibleCells and indexPathsForVisibleItems.
- Examine the order of the items that are returned.
- Scroll the collection view so that cells are reused.
- Examine the order of the items that are returned by visibleCells and indexPathsForVisibleItems again.
Expected Results:
The collection view's visibleCells and indexPathsForVisibleItems return arrays of items whose order matches the order of the cells within the collection view.
Actual Results:
Before any cells are reused both methods return arrays of items whose order does match the order of the cells in the collection view - eg.
indexPathsForVisibleItems:
<__NSArrayM 0x756ed60>(
<NSIndexPath 0x817e470> 2 indexes [0, 0],
<NSIndexPath 0x817e5f0> 2 indexes [0, 1],
<NSIndexPath 0x817e6a0> 2 indexes [0, 2],
<NSIndexPath 0x817e750> 2 indexes [0, 3]
)
visibleCells:
<__NSArrayM 0x756bad0>(
1 : <LabelCell: 0x81809f0; baseClass = UICollectionViewCell; frame = (0 32; 226 236); clipsToBounds = YES; opaque = NO; layer = <CALayer: 0x8180b20>>,
2 : <LabelCell: 0x8186eb0; baseClass = UICollectionViewCell; frame = (236 32; 226 236); clipsToBounds = YES; opaque = NO; layer = <CALayer: 0x8186f40>>,
3 : <LabelCell: 0x81886e0; baseClass = UICollectionViewCell; frame = (472 32; 226 236); clipsToBounds = YES; opaque = NO; layer = <CALayer: 0x8188680>>,
4 : <LabelCell: 0x81869c0; baseClass = UICollectionViewCell; frame = (708 32; 226 236); clipsToBounds = YES; opaque = NO; layer = <CALayer: 0x8186a50>>
)
After the collection view has scrolled enough to reuse a cell, the order of items returned by these methods no longer matches the order in which the items appear in the collection view. eg.
indexPathsForVisibleItems:
<__NSArrayM 0x756bad0>(
<NSIndexPath 0x71911a0> 2 indexes [0, 6],
<NSIndexPath 0x817e6a0> 2 indexes [0, 2],
<NSIndexPath 0x817e750> 2 indexes [0, 3],
<NSIndexPath 0x817e800> 2 indexes [0, 4],
<NSIndexPath 0x71b56e0> 2 indexes [0, 5]
)
visibleCells:
<__NSArrayM 0x7572450>(
7 : <LabelCell: 0x8186eb0; baseClass = UICollectionViewCell; frame = (1416 32; 226 236); clipsToBounds = YES; opaque = NO; layer = <CALayer: 0x8186f40>>,
3 : <LabelCell: 0x81886e0; baseClass = UICollectionViewCell; frame = (472 32; 226 236); clipsToBounds = YES; opaque = NO; layer = <CALayer: 0x8188680>>,
4 : <LabelCell: 0x81869c0; baseClass = UICollectionViewCell; frame = (708 32; 226 236); clipsToBounds = YES; opaque = NO; layer = <CALayer: 0x8186a50>>,
5 : <LabelCell: 0x71b1910; baseClass = UICollectionViewCell; frame = (944 32; 226 236); clipsToBounds = YES; opaque = NO; layer = <CALayer: 0x7180140>>,
6 : <LabelCell: 0x81809f0; baseClass = UICollectionViewCell; frame = (1180 32; 226 236); clipsToBounds = YES; opaque = NO; layer = <CALayer: 0x8180b20>>
)
Notes:
I've attached an Xcode project that demonstrates this bug.
If the API is behaving as it was designed to, then I think either the signature of these methods should change so that they return an unordered set rather than an ordered array or the documentation should be updated to make it clear not to rely on the order of items returned by these methods.
</bug report>
It could be that I'm just mis-understanding the API. That's partly why I'm posting this here too. If I'm doing something wrong, please let me know in the comments below!
Alternatively, this is a real bug and I have to work around it until it is fixed. Here's my simple workaround if you encounter this yourself. I simply use the 'indexPathsForVisibleItems' method that returns an array of index paths and then I sort that array by the index paths. Typical bug: it took hours to figure out what was going wrong and minutes to work-around once I knew where I'd gone wrong.
NSArray *indexPathsOfVisibleStories = [self.collectionView indexPathsForVisibleItems];
NSArray *sortedIndexPaths = [indexPathsOfVisibleStories sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) {
NSIndexPath *path1 = (NSIndexPath *)obj1;
NSIndexPath *path2 = (NSIndexPath *)obj2;
return [path1 compare:path2];
}];
You can download the sample Xcode project if you're interested in seeing this bug in action.