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.
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.
- Get the selected
Superhero
object (selectedHero
) from the diffable data source using the index path. - Append “★” to
selectedHero
‘sname
. - Make a copy of the current diffable data source snapshot, so that we can modify it later.
- Modify the new copy of diffable data source snapshot by reloading
selectedHero
within it. - 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
- How to Reload the Diffable Section Header
- UICollectionView List with Custom Cell and Custom Configuration
- Designing Custom UICollectionViewListCell in Interface Builder
- Building an Expandable List Using UICollectionView: Part 1
- Building an Expandable List Using UICollectionView: Part 2
👋🏻 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.