You are currently viewing Table and Collection View Cells Reload Improvements in iOS 15

Table and Collection View Cells Reload Improvements in iOS 15

In October 2020, I published an article that discusses how to reload a table and collection view cell when using diffable data source. The article shows you how to use 2 totally different approaches when reloading cells with reference and value type item.

As a recap, for references type items, we can leverage the snapshot’s reloadItems(_:) method to reload the specified items. Whereas for value type items, reloadItems(_:) will not work, we will have to manually replace the updated items within the snapshot.

I was always confused why Apple makes such a simple task so complicated. This year, in WWDC21, Apple shows us another way of using diffable data source that makes using 1 single way to reload a cell possible. On top of that, Apple also introduced a new API in iOS 15 that allows us to reload a cell more efficiently.

Those are some very interesting changes and improvements, so let’ get right into it!


The Sample App

For demonstration purposes, let’s update the sample app I created for my previous article. It is a superhero rating app, every time when a user taps on a cell, a star symbol (★) will be appended at the end of the hero’s name.

Sample app to demonstrate cell reload for diffable data source in iOS 15
The sample app

Following are the definition of the collection view’s data source and model objects array:

var dataSource: UICollectionViewDiffableDataSource<Section, Superhero>!

let heroArray = [
    Superhero(name: "Spider-Man"),
    Superhero(name: "Superman"),
    Superhero(name: "Batman"),
    Superhero(name: "Captain America"),
    Superhero(name: "Thor"),
    Superhero(name: "Wonder Woman"),
    Superhero(name: "Iron Man"),
]

Reloading Reference Type Items

Prior to iOS 15, we can reload a cell with reference type item using the snapshot’s reloadItems(_:) method like so:

func collectionView(_ collectionView: UICollectionView,
                    didSelectItemAt indexPath: IndexPath) {
    
    // Get selected hero using index path
    guard let selectedHero = dataSource.itemIdentifier(for: indexPath) else {
        collectionView.deselectItem(at: indexPath, animated: true)
        return
    }
    
    // Update selectedHero
    selectedHero.name = selectedHero.name.appending(" ★")

    // Create a new copy of data source snapshot for modification
    var newSnapshot = dataSource.snapshot()
    
    // Reload selectedHero in newSnapshot
    newSnapshot.reloadItems([selectedHero])
    
    // Apply snapshot changes to data source
    dataSource.apply(newSnapshot)
}

In iOS 15, Apple introduced a new reconfigureItems(_:) snapshots method that helps developers to reload a cell more efficiently. To use it, we can simply replace the reloadItems(_:) method with the reconfigureItems(_:) method:

var newSnapshot = dataSource.snapshot()
newSnapshot.reconfigureItems([selectedHero])
dataSource.apply(newSnapshot)

As you can see from the animated GIF below, when I try to rapidly tap on the same cell, the cell in iOS 15 (using reconfigureItems(_:)) reload a lot faster than the cell in iOS 14 (using reloadItems(_:)).

reloadItems(_:) and reconfigureItems(_:) speed comparison
reloadItems(_:) vs reconfigureItems(_:)

The reason behind this improvement is because reconfigureItems(_:) reuses the item’s existing cell, rather than dequeuing and configuring a new cell. Therefore, from iOS 15 onwards, developers should always use reconfigureItems(_:) instead of reloadItems(_:) unless you have an explicit need to replace the existing cell with a new cell.


Reloading Value Type Items

As mentioned earlier, the reloadItems(_:) method does not work on value type items. But how about reconfigureItems(_:)? Does it work on value type items? Unfortunately, the answer is no. You will get NSInternalInconsistencyException exception with the reason “Invalid item identifier specified for reload“ just like in iOS 14.

However, in this year’s WWDC, Apple shows us another way of using diffable data source which makes using reconfigureItems(_:) on value type items possible. The deal is to use the identifiers of your model objects as the data source item identifiers, and not the model objects themselves.

Diffable data source is built to store identifiers of items in your model, and not the model objects themselves.

“Make blazing fast lists and collection views”, WWDC21

This is somewhat conflict with our understanding on how to use a diffable data source. 🤔

But for now, let’s just put that aside and explore what we can do in order to make that happen.

Using Model Identifier as Diffable Data Source Item Identifier

The first thing we need to do is to add a unique & immutable identifier to our SuperHero struct. We will name the identifier as id and use the superhero’s name as the identifier.

Since we are now using id as our data source item identifier, we also need to change our data source item identifier type from SuperHero to String.

// Add a constant named `id` as identifier
struct Superhero: Hashable {
    let id: String
    var name: String
    
    init(name: String) {
        self.id = name
        self.name = name
    }
}

// Change item identifier type from `SuperHero` to `String`
var dataSource: UICollectionViewDiffableDataSource<Section, String>!

Next up, we will have to declare a dictionary and use it to hold the identifiers and their corresponding SuperHero objects. We need this dictionary so that we can easily get the SuperHero object using id without having to loop through the heroArray.

var heroDictionary = [String: Superhero]()

In order to populate the heroDictionary, we can leverage the Dictionary(uniqueKeysWithValues:) initializer to help us convert [SuperHero] to [(String, SuperHero)] and then to [String: SuperHero]. Here’s how:

// Convert `[SuperHero]` to `[(String, SuperHero)]`
let heroIdNameTupleArray = heroArray.map { ($0.id, $0) }

//Convert `[(String, SuperHero)]` to `[String: SuperHero]`
heroDictionary = Dictionary(uniqueKeysWithValues: heroIdNameTupleArray)

With the heroDictionary in place, we can now proceed to change the cell registration item type from SuperHero to String. On top of that, we also need to make adjustments to the cell registration handler because it is now providing id and not the model object.

let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, String>
{ [unowned self] (cell, indexPath, id) in
    
    var content = cell.defaultContentConfiguration()
    
    // Obtain superhero's name using `id`
    content.text = heroDictionary[id]!.name
    
    // Assign content configuration to cell
    cell.contentConfiguration = content
}

Within the cell registration handler, notice how I use the heroDictionary to obtain the corresponding superhero name.

The last part of the puzzle is to update how we populate the data source snapshot. Instead of adding the model objects to the snapshot, we now need to append the model object ids to the snapshot.

var snapshot = NSDiffableDataSourceSnapshot<Section, String>()
snapshot.appendSections([.main])

// Append all `id` as snapshot items
snapshot.appendItems(heroArray.map { $0.id }, toSection: .main)

dataSource.apply(snapshot, animatingDifferences: false)

With that, we have successfully converted our sample app to use the model object’s identifier as the data source item identifier.

Updating Cells Using `reconfigureItems()`

With the new data source in place, the way to update a cell for value type items is very similar to reference type items. The only difference is that right now the data source is giving us id instead of the model object. Therefore, we will have to get the selected hero using the heroDictionary and update it accordingly.

func collectionView(_ collectionView: UICollectionView,
                    didSelectItemAt indexPath: IndexPath) {
    
    // Get `id` of selected hero using index path
    guard let selectedId = dataSource.itemIdentifier(for: indexPath) else {
        collectionView.deselectItem(at: indexPath, animated: true)
        return
    }

    // Update the selected hero's name and then update `heroDictionary`
    var updatedHero = heroDictionary[selectedId]!
    updatedHero.name = updatedHero.name.appending(" ★")
    heroDictionary[selectedId] = updatedHero

    // Create a new copy of data source snapshot for modification
    var newSnapshot = dataSource.snapshot()

    // Specify the data of items that needs to be updated in `newSnapshot` using `SuperHero.id`
    newSnapshot.reconfigureItems([selectedId])

    // Apply `newSnapshot` to data source so that the changes will be reflected in the collection view.
    dataSource.apply(newSnapshot)

    // Deselect the cell
    collectionView.deselectItem(at: indexPath, animated: true)
}

That’s it! We can now enjoy the performance improvement brings by reconfigureItems() even if using value type items.

Note:

The ID as data source item identifier approach does not only work for value type items, it works for reference type items as well.

Here’s the full sample code for your references:

import UIKit

class ReloadValueTypeViewController: UIViewController {

    struct Superhero: Hashable {
        let id: String
        var name: String
        
        init(name: String) {
            self.id = name
            self.name = name
        }
    }
    
    var collectionView: UICollectionView!
    var dataSource: UICollectionViewDiffableDataSource<Section, String>!
    
    let heroArray = [
        Superhero(name: "Spider-Man"),
        Superhero(name: "Superman"),
        Superhero(name: "Batman"),
        Superhero(name: "Captain America"),
        Superhero(name: "Thor"),
        Superhero(name: "Wonder Woman"),
        Superhero(name: "Iron Man"),
    ]
    
    var heroDictionary = [String: Superhero]()
    
    override func viewDidLoad() {
        super.viewDidLoad()

        self.title = "Reload Value Type"
        
        // Generate `heroDictionary` with `SuperHero.id` as key and `SuperHero` as value
        // This is needed for cell registration
        let heroIdNameTupleArray = heroArray.map { ($0.id, $0) }
        heroDictionary = Dictionary(uniqueKeysWithValues: heroIdNameTupleArray)
        
        // MARK: Setup list layout
        let layoutConfig = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
        let listLayout = UICollectionViewCompositionalLayout.list(using: layoutConfig)
        
        // MARK: Setup collection view
        collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: listLayout)
        collectionView.delegate = self
        view.addSubview(collectionView)
        
        // MARK: Make collection view take up the entire view
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            collectionView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor, constant: 0.0),
            collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0.0),
            collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0.0),
            collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0.0),
        ])
        
        // MARK: Cell registration
        let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, String>
        { [unowned self] (cell, indexPath, id) in
            
            var content = cell.defaultContentConfiguration()
            
            // Obtain superhero's name using `id`
            content.text = heroDictionary[id]!.name
            
            // Assign content configuration to cell
            cell.contentConfiguration = content
        }
        
        // MARK: Setup data source
        dataSource = UICollectionViewDiffableDataSource<Section, String>(collectionView: collectionView, cellProvider: { (collectionView, indexPath, id) -> UICollectionViewCell? in
            
            let cell = collectionView.dequeueConfiguredReusableCell(using: cellRegistration,
                                                                    for: indexPath,
                                                                    item: id)
            
            return cell
        })
        
        // MARK: Data source snapshot
        var snapshot = NSDiffableDataSourceSnapshot<Section, String>()
        snapshot.appendSections([.main])
        // Append all `id` as snapshot items
        snapshot.appendItems(heroArray.map { $0.id }, toSection: .main)
        dataSource.apply(snapshot, animatingDifferences: false)
    }

}

extension ReloadValueTypeViewController: UICollectionViewDelegate {
    
    func collectionView(_ collectionView: UICollectionView,
                        didSelectItemAt indexPath: IndexPath) {
        
            // Get id of selected hero using index path
            guard let selectedId = dataSource.itemIdentifier(for: indexPath) else {
                collectionView.deselectItem(at: indexPath, animated: true)
                return
            }
    
            // Update the selected hero's name and then update `heroDictionary`
            var updatedHero = heroDictionary[selectedId]!
            updatedHero.name = updatedHero.name.appending(" ★")
            heroDictionary[selectedId] = updatedHero
    
            // Create a new copy of data source snapshot for modification
            var newSnapshot = dataSource.snapshot()
    
            if #available(iOS 15, *) {
                // iOS 15
                // Specify the data of items that needs to be updated in `newSnapshot` using `SuperHero.id`
                newSnapshot.reconfigureItems([selectedId])
            } else {
                // iOS 14
                newSnapshot.reloadItems([selectedId])
            }
    
            // Apply `newSnapshot` to data source so that the changes will be reflected in the collection view.
            dataSource.apply(newSnapshot)
    
            // Deselect the cell
            collectionView.deselectItem(at: indexPath, animated: true)
    }
}

Are We Using It Wrongly All This While?

By using ID as the data source item identifier, we are now able to use reconfigureItems(_:) to update both value and reference type items. This makes me wonder: can we apply the same changes and make reloadItems(_:) work for both value and reference type items in iOS 14?

Apparently the answer is yes!

If so, does it mean that we are using diffable data source wrongly all this while?

Well, I am not sure. 🤷🏻‍♂️

Since Apple has made the statement that we should only store the identifier in the diffable data source, I guess we should stop using model objects as the item identifier from now on. On the other hand, I also don’t see anything wrong with using model objects as the item identifier. In fact, the old cell reloading approach presented in this article still works perfectly fine in iOS 15.

Therefore, my take on this is to leave all my existing diffable data source as it is and start adopting the new approach recommended by Apple for any future implementations.


Wrapping Up

The reconfigureItems(_:) method is just one of the many improvements Apple made on diffable data source. To find out more, I would recommend everyone to check out this WWDC21 video, as well as the sample code that comes along with the presentation.


If you enjoy reading this article, feel free to check out my other iOS development related articles. You can also follow me on Twitter, and subscribe to my monthly newsletter.

Thanks for reading. 👨🏻‍💻


👋🏻 Hey!

While you’re still here, why not check out some of my favorite Mac tools on Setapp? They will definitely help improve your day-to-day productivity. Additionally, doing so will also help support my work.

  • Bartender: Superpower your menu bar and take full control over your menu bar items.
  • CleanShot X: The best screen capture app I’ve ever used.
  • PixelSnap: Measure on-screen elements with ease and precision.
  • iStat Menus: Track CPU, GPU, sensors, and more, all in one convenient tool.