Lukas Z's Blog

Calculating the Deltas for performBatchUpdates

Poly Art Title

Time for a code snippet post.

If you have collection or table view that can change in unpredictable ways, and you wish to animate the changes, then you will usually use the performBatchUpdates method of UICollectionView and UITableView.

This method takes a block, and in that block you are expected to account for all the changes in your datasource. For instance, if the number of elements returned in a section is 20 and after the performBatchUpdates-call its 10, they you are required to either delete 10 rows or items within that block, or delete 11 and add 1, or delete 20 and add 10, and so forth. The numbers must add up, otherwise your app will crash with an NSInternatlInconsistencyException. (You can theoretically catch it but I haven’t tried because I suspect it will lead to many odd bugs if you do.)

Ok, so I googled for a generic way to calculate the necessary accounting, and I found this answer on StackOverflow.

The StackOverflow soltuon is presented in pseudocode, so here’s how it would look like with Swift.

import Foundation

enum ArrayAction {
    case added(Int)
    case deleted(Int)
    case moved(Int, Int)
    
    static func operationsPerformed(arrayBefore before: [Int], arrayAfter after: [Int]) -> [ArrayAction] {
        var arrayActions = [ArrayAction]()
        
        var map1 = [Int: Int]()
        var map2 = [Int: Int]()
        
        for (index, element) in before.enumerated() {
            map1[element] = index
        }
        
        for (index, element) in after.enumerated() {
            map2[element] = index
        }
        
        for key in map1.keys.sorted() {
            let index1 = map1[key]
            let index2 = map2[key]
            
            if index2 == nil {
                let deleteAction = ArrayAction.deleted(index1!)
                arrayActions.append(deleteAction)
            }
            else if index1 != index2 {
                let moveAction = ArrayAction.moved(index1!, index2!)
                arrayActions.append(moveAction)
                map2.removeValue(forKey: key)
            } else {
                map2.removeValue(forKey: key)
            }
        }
        
        for key in map2.keys {
            let addAction = ArrayAction.added(map2[key]!)
            arrayActions.append(addAction)
        }
        
        return arrayActions
    }
}

So as you can see there’s an enum called ArrayAction that represents the rows/items added, deleted and moved. Here’s how I use that method in my ViewController:

 private func performCollectionViewOperations(block: () -> (), completionBlock: (()->())? = nil) {
        collectionView.performBatchUpdates({
            let filteredBefore = self.dataSource.elements.map { $0.index }
            block()
            let filteredAfter = self.dataSource.elements.map { $0.index }
            
            for action in ArrayAction.operationsPerformed(arrayBefore: filteredBefore, arrayAfter: filteredAfter) {
                switch action {
                case .added(let i):
                    self.collectionView.insertItems(at: [IndexPath(item: i, section: 0)])
                case .deleted(let i):
                    self.collectionView.deleteItems(at: [IndexPath(item: i, section: 0)])
                case .moved(let i, let j):
                    self.collectionView.moveItem(at: IndexPath(item: i, section: 0), to: IndexPath(item: j, section: 0))
                }
            }
        }) { _ in
            completionBlock?()
        }
    }

And that private method is used like that:

    self.performCollectionViewOperations(block: {
        // do some adding, removing and moving of elements here.
    }) // not using the completionBlock here

And that’s pretty much it.

Where do I use this code and might it have anything to do with the title picture? Glad you asked. :P

It’s used in Poly Art, a game a friend and I made for iOS. It’s free to play and we’d be happy if you gave it a try!

(I really had a hard time calculating the deltas in the game, because I stubbornly avoided the generic solution. Instead I was trying to anticipate what changes were possible, but as any programmer sooner or later learns: Even the simplest things can get very difficult if you use just a few of them combined.)

P.S.: You can follow me on Twitter.

Comments

Webmentions