When inserting multiple custom supplementary views in a collection view with existing supplementary views, using a subclass of UICollectionViewFlowLayout, the collection view appears to create glitched animations or otherwise not handle the insertions correctly.
Inserting one supplementary view behaves as expected, but inserting two supplementary views at once results in an obvious visual glitch, and inserting many supplementary views at once (e.g. 4+) worsens the glitch. (iOS 10.2, iOS 9.3, Swift 3, Xcode 8.2.1)
Here's a demonstration:
You can reproduce the problem using this sample project. In this simplified example, there is one supplementary view for each item, and two items (with two supplementary views) are inserted during each batch update. In my real project I have fewer supplementary views than items and I utilise other flow layout features.
My best guess is that something is causing the collection view to confuse the index paths or layout attributes corresponding to existing/inserted supplementary views. If this turns out to be a bug in one of Apple's classes, I would be very grateful for your most elegant workaround.
Attempted solutions
I've tried and double-checked each of the following:
Implementing indexPathsToInsertForSupplementaryView(ofKind:)
—so far as I can tell, this is straightforward and my implementation returns the correct index paths. The views are always inserted. For supplementary header and footer views, UICollectionViewFlowLayout seems to return a sorted array of index paths, but sorting the array hasn't made any difference for me.
Providing initial and final layout attributes—calling super
for these methods appears to return the correct frames, so it is not obvious what adjustments (if any) could be made to the initial or final layout attributes to solve the problem. But see the third curiosity noted below.
Invalidating the layout—it seems to make no difference at all whether the layout is manually invalidated in whole or in part before, during, or after performBatchUpdates()
, or at all.
Custom index paths—although index paths for supplementary elements do not need to correspond to item index paths, so far as I can tell, and despite documentation to the contrary, UICollectionViewFlowLayout will crash (by requesting layout attributes for non-existent index paths) unless supplementary elements of the same kind are numbered sequentially from 0. I assume this is how the collection view and/or the layout object is able to compute new index paths when elements are inserted or deleted (i.e. by incrementing or decrementing the item
property of the index path). So constructing arbitrary index paths is not an option.
Curiosities noticed while debugging
The visual glitches are specific to supplementary views and do not occur for inserted items, even when the index paths and frames are the same for both.
Debugging the view hierarchy reveals that, although the frames of the inserted supplementary views are correct, the frames of some existing supplementary views are not correct. That is so despite the fact that the frames returned by the layout object for the existing views at those index paths appear to be correct.
When inserting multiple supplementary views, the calls made by the collection view for initial and final layout attributes appear to be unbalanced. That is, initial layout attributes are requested multiple times for the same index path (and, naturally, the same attributes are returned each time), and there are fewer requests for final layout attributes. Further, for some existing supplementary views, initial and final layout attributes are never requested at all. I find this suspicious. It leads me to believe that the collection view is somehow confusing supplementary views and/or index paths before and after the update.
The memory addresses for instances of IndexPath created by my Swift code are strikingly different to the instances of NSIndexPath created internally by UICollectionView and UICollectionViewFlowLayout. The former are always different (as expected), while the latter appear to be identical between launches (e.g. [0, 0], [0, 1] and [0, 2] are always at 0xc000000000000016
, 0xc000000000200016
and 0xc000000000400016
). In Swift, IndexPath is bridged to NSIndexPath, but the former is a value type whereas the latter is a reference type. NSIndexPath also uses tagged pointers. There is a remote possibility that the two have been imperfectly bridged and/or that the collection view classes rely internally on NSIndexPath behaviour in some way that gives rise to the index path confusion apparent here. I don't know how to test this or take this further.
Similar questions
The following questions may be related, although none of the answers worked for me:
How can UICollectionView supplementary views be inserted or deleted correctly
layoutAttributesForSupplementaryViewOfKind:atIndexPath: passes in incorrect indexPath
UICollectionView supplementary views won't animate “in” or “out”
UICollectionView Decoration and Supplementary views can not be moved
The problem may also be related to this bug report on Open Radar. The sample project accompanying that report is a bit too complicated to be of much use though.
Apple Technical Support
After posting this question, I submitted a Technical Support Incident to Apple. Apple Developer Support replied as follows:
Our engineers have reviewed your request and have determined that this would be best handled as a bug report.
Please submit a complete bug report regarding this issue using the Bug Reporting tool at https://developer.apple.com/bug-reporting/.
...
We’ve spent some time investigating the possibility of a workaround and unfortunately have come up empty handed. Please follow up with your bug. If we find the possibility of some kind of workaround in the future, we will reach out. Sorry I don’t have better news.
If you experience this problem, please duplicate rdar://30510010.
Below is the output of the CustomCollectionViewController
file indexPath item Row is for cellForItemAt indexPath
and Row Supplementary is for viewForSupplementaryElementOfKind
.
As you can see cellForItemAt
is properly returning the indexpath.item
but in the viewForSupplementaryElementOfKind
is not returning the expected answer. I didn't get the reason upto now, if I get i update you soon.
So for re-solve you can deal with array by adding new data in it and reload collectionView instead of indexPath.
I'm not sure if I have a correct solution for the initial issue but I had a same problem like in the answer from @agent_stack.
If you have a custom collection view layout (subclass of UICollectionViewLayout or subclass of UICollectionViewFlowLayout) and you add your own supplementary view then you have to override an additional method: indexPathsToInsertForSupplementaryView(ofKind elementKind: String)
The collection view calls this method whenever you add cells or sections to the collection view. Implementing this method gives your layout object an opportunity to add new supplementary views to complement the additions.
I did my own CollectionViewFlowLayout and only after adding this methods in combination everything is animating as it should!
override func prepare(forCollectionViewUpdates updateItems: [UICollectionViewUpdateItem]) {
super.prepare(forCollectionViewUpdates: updateItems)
// Prepare update by storing index path to insert
// ────────────────────────────────────────────────────────────
// IMPORTANT: Item vs. Section Update
//
// An instance of UICollectionViewUpdateItem can represent either an item or a section update,
// and the only way to tell which is that update items which represent section updates contain
// index paths with a valid section index and NSNotFound for the item index, while update items
// that represent item updates contain index paths with valid item indexes.
//
// This appears to be completely undocumented by Apple, but it is absolutely consistent behavior.
// https://www.raizlabs.com/blog/2013/10/animating_items_uicollectionview/
var indexPaths = [IndexPath]()
for updateItem in updateItems {
switch updateItem.updateAction {
case .insert:
// If it is a section update then convert NSNotFound to zero.
// In our case a section update always has only the first item.
if var indexPath = updateItem.indexPathAfterUpdate {
if indexPath.item == NSNotFound { indexPath.item = 0 }
indexPaths.append(indexPath)
}
default:
break
}
}
indexPathsToInsert = indexPaths
}
override func indexPathsToInsertForSupplementaryView(ofKind elementKind: String) -> [IndexPath] {
// Extra add supplementary index paths
// ────────────────────────────────────────────────────────────
// The collection view calls this method whenever you add cells or sections to the collection view.
// Implementing this method gives your layout object an opportunity to add new supplementary views
// to complement the additions.
// http://stackoverflow.com/a/38289843/7441991
switch elementKind {
case UICollectionElementKindDateSeparator:
return indexPathsToInsert
case UICollectionElementKindAvatar:
return indexPathsToInsert
default:
// If it is not a custom supplementary view, return attributes from flow layout
return super.indexPathsToInsertForSupplementaryView(ofKind: elementKind)
}
}
Feel free to contact me, if you have any questions! :)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With