Apple did a tremendous job in giving developers such powerful building blocks as UITableView and UICollectionView. It’s even possible to claim that iOS wouldn’t have been such a success without these general purpose views. But unfortunately when it comes time to update these views in a batch fashion, it appears to be surprisingly hard. If you’ve tried to do it, you’re likely to be familiar with internal exceptions stating that the data model is not in sync with requested updates.
This article is about our attempts to update the mentioned views correctly according to obscure rules, in order to avoid these exceptions leading to crashes in runtime.
What is a batch update and when is it needed?
A batch update is a set of the following operations:
It is applied for both items and sections in the view which are combined and executed together, perhaps in an animated fashion.
UICollectionView allows us to perform batch updates using the
- [UICollectionView performBatchUpdates:completion:] method. For instance, to reload and insert items at particular index paths and remove the first section, it may look as follows:
In other words, all operations are to be specified in the given block, and it’s UIKit business to generate corresponding animations for us.
For UITableView it’s possible to do the same the but with additional calls of
- [UITableView beginUpdates]-
- [UITableView endUpdates] methods. It is even possible to specify which operations are to be animated and which are not:
UIKit does not allow us to track finishing of animations for UITableView out of the box like for UICollectionView, but fortunately it can be achieved easily using the CoreAnimation framework which drives the mentioned animations under the hood:
As mentioned above, more often than not when you try to perform more or less complex batch updates, you’ll end up receiving an internal inconsistency exception like this:
*** Terminating app due to uncaught exception ‘NSInternalInconsistencyException’, reason: ‘attempt to insert item 2 into section 0, but there are only 2 items in section 0 after the update’
Under some circumstances it may even lead to memory issues with internal UIKit entities used for updates:
If such issues appear you should consider yourself lucky, as even if you don’t get them users of your application might, and this will lead to crashes and plenty of disappointment.
Something weird is happening…
Let’s read the documentation (again!)
Apparently something’s being done wrong, so let’s revisit documentation and read how batch updates are to be performed again:
Deletes are processed before inserts in batch operations. This means the indexes for the deletions are processed relative to the indexes of the collection view’s state before the batch operation, and the indexes for the insertions are processed relative to the indexes of the state after all the deletions in the batch operation.
This is helpful, but not very helpful, as even if the batch updates are performed in accordance with this statement it won’t work out as expected.
So let’s just work around these issues by using
@try/@catch blocks to suppress propagation of internal exceptions and thus abnormal termination because of unhandled exceptions:
This seems to be promising, but it fact it does not work out as the view appears in an incorrect internal state, meaning it’s not possible to interact with it in a predicted way after that. We are left with an approach like:
Just try everything possible and if it something does not work, make a workaround for it.
Found solutions and workarounds
An initial implementation of the update algorithm is implemented and various combinations of update operations are simulated on both a simulator and a real device. Once an issue is revealed the algorithm is adjusted and a test case scenario is implemented. In this scenario, we’ve noticed the following:
Simultaneous updates of sections and items lead to the mentioned exceptions and incorrect internal states of views, so once section updates are detected, views are reloaded completely and without animations using
-reloadDatamethods. This is a significant limitation of this approach.
Reloads can not be used in conjunction with other changes, as under some circumstances they lead to memory corruption issues with internal UIKit entities. This has been worked around by asking a corresponding data source to update a specified cell in a way it’s reloaded (updated) when reused.
With that, if sections are NOT added, removed or moved often, consider reusing our solution.
In order to generalise our solution, all collection items and sections are supposed to conform to the following protocols, respectively:
Once both old and new data models are available, it’s possible to calculate necessary updates and apply them using extension methods of the UITableView and UICollectionView classes.
Please note that when a full reload of view is needed, the array of updates specified in the block is
nil, so once they are propagated to extensions methods the reload is done automatically. Implementation of
- [Controller reloadCell:atIndexPath:] is to be the same as for the corresponding data source method:
- tableView:cellForRowAtIndexPath: or
Please use our approach if it’s applicable to your needs, and do share your ideas or criticisms by adding comments to this article.