Recently, I am working on an iOS project that has an UICollectionView
in it. For updating items in the collection, I wrote code like this
collectionView.performBatchUpdates({
for update in updates {
switch update {
case .Add(let index):
collectionView.insertItemsAtIndexPaths([NSIndexPath(forItem: index, inSection: 0)])
case .Delete(let index):
collectionView.deleteItemsAtIndexPaths([NSIndexPath(forItem: index, inSection: 0)])
}
}
}, completion: nil)
Basically, whenever the data source updates the items, it runs this piece of code to insert or delete items in the UICollectionView
. The code looks pretty straightforward, but sometimes it crashes. The exception looks like this
Invalid update: invalid number of items in section 0. The number of items contained in an existing section after the update (1) must be equal to the number of items contained in that section before the update (1), plus or minus the number of items inserted or deleted from that section (1 inserted, 0 deleted) and plus or minus the number of items moved into or out of that section (0 moved in, 0 moved out).
Really odd right? I checked the number in data source, it’s correct. I thought it could be bug, then I googled around and found this on stackoverflow. It said that there is a bug in UICollectionView
, to workaround that, you need to call reloadData
when it’s empty, roughly like this:
if collection empty {
collectionView.reloadData()
} else {
collectionView.performBatchUpdates({
for update in updates {
switch update {
case .Add(let index):
collectionView.insertItemsAtIndexPaths([NSIndexPath(forItem: index, inSection: 0)])
case .Delete(let index):
collectionView.deleteItemsAtIndexPaths([NSIndexPath(forItem: index, inSection: 0)])
}
}
}
}
Problem still not solved
Although I tried to reloadData()
first then insert and delete, I still see crashes on the performBatchUpdates
method call. It’s not really well-documented how performBatchUpdates
works in details, so I decided to try it out to understand how it works.
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
print("Get numberOfItemsInSection", section, items.count)
return items.count
}
Then I saw something like
Get numberOfItemsInSection 0 1
collectionView.performBatchUpdates closure called
Get numberOfItemsInSection 0 1
So it turned out performBatchUpdates
calls collectionView(_:numberOfItemsInSection:)
first before call the given closure to know the item numbers. Next, it calls the closure, and eventually, it calls collectionView(_:numberOfItemsInSection:)
again to check the number. And here is where the assertion exception thrown. Say if we insert a new item, and it sees
Okay, before one insert, the total item count is 1, let’s update.
Then
Job done, let’s check the item count again, wait, WTF? it’s still 1? Impossible, I just inserted one item!
When the story comes to this point, I finally understand why it throws that exception. My data source updates its item count first, then performBatchUpdates
was called to update the UICollectionView
. But the problem is, collectionView(_:numberOfItemsInSection:)
returns the post-update item count, it confuses collectionView.performBatchUpdates
why the item number is not changed correctly according to the updates we just did.
The solution
As if my understanding to how performBatchUpdates
is correct, the item count returned by collectionView(_:numberOfItemsInSection:)
should be sync with the updates made inside the closure. With this idea in mind, it’s easy to solve, just add a property as the item count and update it inside performBatchUpdates
closure
func updateItems(updates: [ItemUpdate]) {
collectionView.performBatchUpdates({
for update in updates {
switch update {
case .Add(let index):
collectionView.insertItemsAtIndexPaths([NSIndexPath(forItem: index, inSection: 0)])
itemCount += 1
case .Delete(let index):
collectionView.deleteItemsAtIndexPaths([NSIndexPath(forItem: index, inSection: 0)])
itemCount -= 1
}
}
}, completion: nil)
}
and for the collectionView(_:numberOfItemsInSection:)
, instead of returning items.count
, we return the property which is manually maintained by performBatchUpdates
closure.
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return itemCount
}
Then problem solved!