Background (Current State of the Art)

Before iOS13, in UIKit UITableView and UICollectionView had to use the following three protocol methods when interacting with data sources:

  • UITableView
@available(iOS 2.0.*)
func tableView(_ tableView: UITableView.numberOfRowsInSection section: Int) -> Int


// Row display. Implementers should *always* try to reuse cells by setting each cell's reuseIdentifier and querying for available reusable cells with dequeueReusableCellWithIdentifier:
// Cell gets various attributes set automatically based on table (separators) and data source (accessory views, editing controls)

@available(iOS 2.0.*)
func tableView(_ tableView: UITableView.cellForRowAt indexPath: IndexPath) -> UITableViewCell


@available(iOS 2.0.*)
optional func numberOfSections(in tableView: UITableView) -> Int // Default is 1 if not implemented
Copy the code
  • UICollectionView
@available(iOS 6.0.*)
func collectionView(_ collectionView: UICollectionView.numberOfItemsInSection section: Int) -> Int

    
// The cell that is returned must be retrieved from a call to -dequeueReusableCellWithReuseIdentifier:forIndexPath:
@available(iOS 6.0.*)
func collectionView(_ collectionView: UICollectionView.cellForItemAt indexPath: IndexPath) -> UICollectionViewCell

    
@available(iOS 6.0.*)
optional func numberOfSections(in collectionView: UICollectionView) -> Int
Copy the code

When rendering a UITableView and a UICollectionView, these methods will return the corresponding data to us in real time according to indexPath to render the corresponding cell we need. Those of you who have been involved in iOS development know that these methods have served us for nearly 10 years, and we have been used to them for a long time. We have been used to their simplicity and directness, and at the same time, we have endured all kinds of puzzling problems caused by them. For example: sometimes when you call performBatchUpdates()

'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (0) must be equal to the number of rows contained in that section before the update (3), plus or minus the number of rows inserted or deleted from that section (0 inserted, 1 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).'
Copy the code

This kind of strange thing happens when synchronization between the UI and the data source fails. This can be resolved temporarily if you call reloadData() instead, but the UI state transitions are unanimated and clunky, which runs counter to good interaction concepts. So, as we can see, the method we used to provide data sources for UITableView and UICollectionView is simple, but error prone.

A new approach

DiffableDataSource, apple’s demo video translates it as a “differential datasource.” There will be no performBatchUpdates(), just simple apply(). Let’s start with a new concept called snapshot. The snapshot can be understood as a certain state at a certain moment in the running process of the APP. Because of the unique identifier, each section and item’s state is unique, which means that each state of a UITableView or UICollectionView is unique during rendering. Therefore, when the data in the section or item is changed, the DiffableDataSource can easily detect and compare the changed data source with the original data source to obtain the difference and render the difference on the interface. The DiffableDataSource calls a simple apply() method to render the old snapshot to the new one. So, the core of DiffableDataSource is the unique identifier, and there is no IndexPath anymore, everything is identifier. There are four classes involved: UITableViewDiffableDataSource, UICollectionViewDiffableDataSource, NSCollectionViewDiffableDataSource, NSDiffableDataSourceSnapshot. On the iOS platform is UITableViewDiffableDataSource and UICollectionViewDiffableDataSource, Mac platforms is NSCollectionViewDiffableDataSource. Gm’s NSDiffableDataSourceSnapshot is all platform.

Practice of new methods

In three steps, as shown in the following code:

  1. Configure the UI
  2. Configuring a Data Source
  3. The data source applies a snapshot to update the UI
    override func viewDidLoad(a) {
        super.viewDidLoad()
        configureTableView()    / / configuration UI
        configDataSource()      // Configure the data source
        updateUI()              // Apply the snapshot to the data source to update the UI
    }
Copy the code

Configure uI-related codes:

    func configureTableView(a) {
        view.addSubview(tableView)
        tableView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            tableView.topAnchor.constraint(equalTo: view.topAnchor),
            tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
            ])
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: ViewController.reuseIdentifier)
    }
Copy the code

It’s very simple, just add a tableView to the ViewController, add the constraint, and register the cell.

Configuring a Data Source

    func configDataSource(a) {
        self.dataSource = UITableViewDiffableDataSource<Section.Item>.init(tableView: self.tableView, cellProvider: {(tableView, indexPath, item) -> UITableViewCell? in
            let cell = tableView.dequeueReusableCell(withIdentifier: ViewController.reuseIdentifier, for: indexPath)
            var content = cell.defaultContentConfiguration()
            content.text = item.title
            cell.contentConfiguration = content
            return cell
        })
        self.dataSource.defaultRowAnimation = .fade
    }
Copy the code

Here is involved in this paper, the role of the UITableViewDiffableDataSource, create a UITableViewDiffableDataSource instance, note that here in the initialization method need to hand in, and the current tableView Send in whoever’s dataSource. UITableViewDiffableDataSource can use section and restrain the generic item. Then, in the Closure callback of cellProvider, you do the cell assignment and other configuration.

The data source applies a snapshot to update the UI

    func updateUI(a) {
        currentSnapshot = NSDiffableDataSourceSnapshot<Section.Item>()
        currentSnapshot.appendSections([.main])
        currentSnapshot.appendItems(mainItems, toSection: .main)
        self.dataSource.apply(currentSnapshot, animatingDifferences: true)}Copy the code

Here to create a snapshot NSDiffableDataSourceSnapshot instance, add section in this snapshot instance data, and add the corresponding item data to the corresponding section. It should be noted that Apple abstracts the data that needs to be passed into the snapshot into identifiers, that is, the identifier data needs to be passed in to meet the uniqueness of the data, so the data needs to meet the hash condition. Sections pass in an enumeration array, swift’s enumeration is automatically hash, and items pass in a custom struct, which implements Hashable protocol, as shown below:

enum Section: CaseIterable {
    case main
}

struct Item: Hashable {
    let title: String

    init(title: String) {
        self.title = title
        self.identifier = UUID()}private let identifier: UUID
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(self.identifier)
    }
}
Copy the code

Ok, so now, the data structure is set up:

    lazy var mainItems: [Item] = {
        return [Item(title: "Heading 1"),
                Item(title: "Heading 2"),
                Item(title: "Title")]
    }()
Copy the code

Apply (currentSnapshot, animatingDifferences) self.datasource. Apply (currentSnapshot, animatingDifferences: True) the list is rendered successfully, and comes with excellent animation.

The demo address