You are currently viewing The Modern Ways to Reload Your Table and Collection View Cells

The Modern Ways to Reload Your Table and Collection View Cells

Updated: 16 June 2021

Apple introduced a better way to reload a cell in WWDC21. If you app only need to support iOS 15 and above, you can proceed to this article: Table and Collection View Cells Reload Improvements in iOS 15

In iOS 13, Apple introduced diffable data source and snapshot, defining the modern era of table view and collection view. Prior to this, reloading a table or collection view cell can be easily done by calling one of the following functions:

reloadRows(at:with:) // For reloading table view cell
reloadItems(at:) // For reloading collection view cell

For table and collection views constructed using a diffable data source, this is no longer true. If so, how should developers go about reloading their table and collection view cells?

The solution to this might not be as straightforward as you think. Due to the difference between value type and reference type, there will be 2 different ways to reload the table and collection view cells.


The Sample App

As usual, let’s take a quick look at the sample app that I will use to showcase the ways to reload a cell.

Reload data in NSDiffableDataSourceSectionSnapshot
The sample app

The sample app is a superhero rating app. When a user taps on a cell, we will append a star symbol (★) at the end of the hero’s name.

Note that we are using a collection view for the sample app, however, the same concept should be able to apply to the table view as well.

Note:

If you’re unfamiliar with the basic concept of list in collection view, I highly recommend another article of mine called “Building a List with UICollectionView in Swift“.


Reloading Reference Type Items

Before getting into the reloading logic, let’s take a look at the diffable data source item identifier type — the Superhero class.

class Superhero: Hashable {

    var name: String

    init(name: String) {
        self.name = name
    }

    // MARK: Hashable
    func hash(into hasher: inout Hasher) {
        hasher.combine(name)
    }

    static func == (lhs: ReloadReferenceTypeViewController.Superhero,
                    rhs: ReloadReferenceTypeViewController.Superhero) -> Bool {
        lhs.name == rhs.name
    }
}

As can be seen, the Superhero class is a simple class with a variable called name.

Do note that we need to explicitly implement the hash(into:) and ==(lhs:rhs:) functions because classes do not support automatic Hashable conformance.

Now that you have seen the Superhero class, let’s get into the main topic of this article — cell reloading.

We will perform cell reloading at the collectionView(_:didSelectItemAt:) delegate method. Here’s how we do it:

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

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

The code above is pretty straightforward.

  1. Get the selected Superhero object (selectedHero) from the diffable data source using the index path.
  2. Append “★” to selectedHero‘s name.
  3. Make a copy of the current diffable data source snapshot, so that we can modify it later.
  4. Modify the new copy of diffable data source snapshot by reloading selectedHero within it.
  5. Apply the snapshot to the diffable data source. The collection view will reflect the snapshot changes.

One big caveat of the above code is that it only works on reference type items. Why is it so?

In order to understand what’s going on, you must first understand the difference between value type and reference type. In short, if Superhero is a value type, selectedHero will be a new instance of Superhero, it will not point to the selected Superhero object within the snapshot. Therefore, if you try to reload selectedHero in newSnapshot, you will get the NSInternalInconsistencyException exception with the reason “Invalid item identifier specified for reload“.

Now that you have understood why the above code can only work on reference type. Let’s switch our focus and find out how you can make the same thing work on value type items.


Reloading Value Type Items

At this point, you might be wondering why some developers might prefer to use value type (struct) as item identifier type instead of reference type (class). There are various reasons for that, and the most significant reason for using struct is that its definition is cleaner and simpler.

struct Superhero: Hashable {
    var name: String
}

As you can see, we have significantly reduced the amount of code in the definition thanks to the help of automatic Hashable conformance and automatic initializer synthesis.

Getting back into the cell reloading code, it is a little bit different from the reference type cell reloading code. As mentioned earlier, value type items will not work on reloadItems(_:). If so, what can we do about that?

Fortunately, we can easily work around that by replacing the selected Superhero object (selectedHero) within the snapshot with a new Superhero object (updatedHero).

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
  }

  // Create a new copy of selectedHero & update it
  var updatedHero = selectedHero
  updatedHero.name = updatedHero.name.appending(" ★")

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

  // Replacing selectedHero with updatedHero
  newSnapshot.insertItems([updatedHero], beforeItem: selectedHero)
  newSnapshot.deleteItems([selectedHero])

  // Apply snapshot changes to data source
  dataSource.apply(newSnapshot)
}

Do note that the code above only works on value type items, if you apply the above code on reference type items, you will get NSInternalInconsistencyException with the reason “Invalid update: destination for insertion operation [struct_instance] is in the insertion identifier list for update“.


Wrapping Up

To be honest, I am not sure why Apple designs the NSDiffableDataSourceSnapshot APIs in such a way that it does not work on both reference and value types.

I suspect it might be due to some technical limitations that we are not aware of. That said, I do hope that Apple will improve the APIs by giving us a standardized way to reload the table and collection view cells.

Feel free to get the full sample code on GitHub.

You can reach out to me on Twitter if you have any questions, thoughts or comments.

Thanks for reading. 👨🏻‍💻


Further Readings


👋🏻 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.