Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UICollectionView performBatchUpdates: asserts unexpectedly if view needs layout?

If I call -[UICollectionView performBatchUpdates:] from inside viewWillAppear:, inside viewDidAppear:, between these methods, or anytime the collection view has not been laid out by the greater UIView view hierarchy, the collection view will assert with:

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of sections. The number of sections contained in the collection view after the update (X) must be equal to the number of sections contained in the collection view before the update (X), plus or minus the number of sections inserted or deleted (X inserted, 0 deleted).'

Where "X" is whatever number of items I've inserted. I've confirmed that the data source is being updated properly and the index paths in the update are correct.

How could the collection view state become consistent with the data source, when the data source was just updated immediately before calling performBatchUpdates:? It seems something is unexpectedly triggering a reloadData.

like image 330
Mat Davidson Avatar asked Nov 12 '14 23:11

Mat Davidson


1 Answers

UICollectionView seems to have special behavior (a bug?): if it needs layout, then performBatchUpdates: effectively acts as a reloadData before calling the update block, which renders whatever changes you plan to make during the update block poisonous to the collection view bookkeeping.

If you plan to have batch updates applied to the view before it's been properly laid out (like from a notification handler in a rapidly changing data model environment for instance), you need to make sure that in viewWillAppear: that you call layoutIfNeeded on the collection view. This will prevent the collection view from reloading in the call to performBatchUpdates:.

We discovered this behavior by putting a log in our collection view data source numberOfSections method and printed out this backtrace to see where it was called from:

2014-11-12 15:30:06.173 CVCrasher[66830:6387719] [CV] #sections stack: (
0   CVCrasher     0x000000010ba9122d -[MyViewController numberOfSectionsInCollectionView:] + 61
1   UIKit         0x000000010cfc2811 -[UICollectionViewData _updateItemCounts] + 147
2   UIKit         0x000000010cfc4a89 -[UICollectionViewData numberOfSections] + 22
3   UIKit         0x000000010cfaebae -[UICollectionViewFlowLayout _getSizingInfos] + 348
4   UIKit         0x000000010cfafca9 -[UICollectionViewFlowLayout _fetchItemsInfoForRect:] + 526
5   UIKit         0x000000010cfab51f -[UICollectionViewFlowLayout prepareLayout] + 257
6   UIKit         0x000000010cfc2a10 -[UICollectionViewData _prepareToLoadData] + 67
7   UIKit         0x000000010cfc30e9 -[UICollectionViewData validateLayoutInRect:] + 54
8   UIKit         0x000000010cf8b7b8 -[UICollectionView layoutSubviews] + 170
9   UIKit         0x000000010c9d1973 -[UIView(CALayerDelegate) layoutSublayersOfLayer:] + 521
10  QuartzCore    0x0000000110cf2de8 -[CALayer layoutSublayers] + 150
11  QuartzCore    0x0000000110ce7a0e _ZN2CA5Layer16layout_if_neededEPNS_11TransactionE + 380
12  UIKit         0x000000010c9c5847 -[UIView(Hierarchy) layoutBelowIfNeeded] + 611
13  UIKit         0x000000010cf9c7b7 -[UICollectionView performBatchUpdates:completion:] + 164
...

Here you can plainly see that the call to performBatchUpdates: is hitting the datasource and becoming consistent with the changed model before the updates are applied. When the block itself is then called, the collection view will throw an assert like I showed in the original question.

tl;dr - When UICollectionView needs layout, performBatchUpdates: effectively acts as a call to reloadData and will make the batch update block assert because of bad bookkeeping. Call layoutIfNeeded in viewWillAppear: to avoid this behavior.

like image 129
Mat Davidson Avatar answered Nov 15 '22 08:11

Mat Davidson