Recently I needed to update the contents of a UICollectionView. Simple right? Just call reloadData and be done with it.

However this results in poor UX because users don’t get the visual feedback of how items got reordered. Of course the solution is to animate insert/delete/move items in the performBatchUpdates block, but how can we get from the old list of items to the new one?

Sample Application

Click to play

Suppose we have an array of integers that we are currently displaying in the collection view. When a user taps on a button, we want to display a different list of integers, potentially including numbers that were present in the original list.

Luckily all of the animation is builtin; the only thing we need to do is tell the collection how to change. The order to accomplish this is to delete, then move, and finally insert.

The code

Here is the high level of what we are going to do. Each step is encapsulated inside its own update block to make it more clear, as well as to ensure our data source is in sync with the internal collection view.

func setCollectionViewItems(newItems: [Int]) {
    // self.items = [0, 1, 2, 3, 4, 5, 6, 7, 8]

    // Delete old items
    collectionView.performBatchUpdates({ ... })

    // Move existing items to correct order
    collectionView.performBatchUpdates({ ... })

    // Insert new items
    collectionView.performBatchUpdates({ ... })
}

Step 1: Delete

One thing to note is that UICollectionView only allows a single delete at any given index path inside a performBatchUpdates block. For example, if we want to delete the first two elemnts, we can’t call delete at index path 0 twice like we could an array.

This means that we cannot simply iterate and delete as we go along, because deleting an element shifts every element after by one. Thus, the indices would be incorrect (bad) and we might call delete twice with the same index (worse!). To get around this, we can simply reverse the iteration order so that we start with the last element, as this does not change future indices that are iterated.

// Delete old items
collectionView.performBatchUpdates({
    // Perform this in reverse to maintain correct indices
    for (i, item) in self.items.enumerate().reverse() {
        guard newItems.indexOf(item) == nil else { continue }
        self.items.removeAtIndex(i)
        self.collectionView.deleteItemsAtIndexPaths([ NSIndexPath(forItem: i, inSection: 0) ])
    }
}, completion: nil)

Step 2: Move

The goal of the second step is to move the existing items into the correct order in the new list. (This can be further optimized by creating a dictionary containing the mapping from item to index instead of performing a linear search every time.)

// Move existing items to correct order
collectionView.performBatchUpdates({
    var index = 0
    for item in newItems {
        guard let fromIndex = self.items.indexOf(item) else { continue }
        self.collectionView.moveItemAtIndexPath(
            NSIndexPath(forItem: fromIndex, inSection: 0), toIndexPath:
            NSIndexPath(forItem: index, inSection: 0))
        index += 1
    }
}, completion: nil)

Step 3: Insert

Finally we compute and create index paths that correspond to the new items, assign the newItems to our instance variable, and give the collection view the array of new indices to insert. (Again, a slight optimization is to compute the new index paths inside the guard statement of the second step.)

// Insert new items
collectionView.performBatchUpdates({
    let newIndexPaths = newItems.enumerate().filter({ (index, item) -> Bool in
        return !self.items.contains(item)
    }).map({ (index, item) -> NSIndexPath in
        return NSIndexPath(forItem: index, inSection: 0)
    })

    self.items = newItems
    self.collectionView.insertItemsAtIndexPaths(newIndexPaths)
}, completion: nil)

Summary

Hopefully this shows that it doesn’t take a lot of additional work to create a much better UX with animation. A demo project is available here.