3

The Modern Ways to Reload Your Table and Collection View Cells

 3 years ago
source link: https://swiftsenpai.com/development/modern-ways-reload-cells/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

The Modern Ways to Reload Your Table and Collection View Cells

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





About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK