Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift DiffableDataSource make insert&delete instead of reload

I have a hard time to understand how DiffableDataSource works. I have ViewModel like this

struct ViewModel: Hashable {
  var id: Int
  var value: String

  func hash(into hasher: inout Hasher) {
     hasher.combine(id)
  }
}

I have tableView populated by cachedItems like ViewModele above. When API response arrives I want to add a new one, delete missing one, refresh viewModel.value of items already present in tableView and finally order it. Everything works fine except one thing - reloading items.

My understanding of DiffableDataSource was that it compares item.hash() to detect if the item is already present and if so then if cachedItem != apiItem, it should reload. Unfortunately, this is not working and snapshot does delete & insert instead of reloading.

Is DiffableDataSource supposed to do that?

Of course, I have a solution - to make it work I need to iterate through cachedItems, when new items contains the same id, I update cachedItem, then I applySnapshot without animation and after then i finally can applySnapshot with animation for deleting/inserting/ordering animation.

But this solution seems to be more like a hack than a valid code. Is there a cleaner way how to achieve this?

UPDATE:

There is the code showing the problem. It should work in playground. For example. items and newItems containt viewModel with id == 0. Hash is the same so diffableDataSource should just reload because subtitle is different. But there is visible deletion / inserting instead reload


import UIKit
import PlaygroundSupport

class MyViewController : UIViewController {
    let tableView = UITableView()

    var  diffableDataSource: UITableViewDiffableDataSource<Section, ViewModel>?

    enum SelectesItems {
        case items
        case newItems
    }

    var selectedItems: SelectesItems = .items

    let items: [ViewModel] = [ViewModel(id: 0, title: "Title1", subtitle: "Subtitle2"),
    ViewModel(id: 1, title: "Title2", subtitle: "Subtitle2"),
    ViewModel(id: 2, title: "Title3", subtitle: "Subtitle3"),
    ViewModel(id: 3, title: "Title4", subtitle: "Subtitle4"),
    ViewModel(id: 4, title: "Title5", subtitle: "Subtitle5")]

    let newItems: [ViewModel] = [ViewModel(id: 0, title: "Title1", subtitle: "New Subtitle2"),
    ViewModel(id: 2, title: "New Title 2", subtitle: "Subtitle3"),
    ViewModel(id: 3, title: "Title4", subtitle: "Subtitle4"),
    ViewModel(id: 4, title: "Title5", subtitle: "Subtitle5"),
    ViewModel(id: 5, title: "Title6", subtitle: "Subtitle6")]

    override func loadView() {
        let view = UIView()
        view.backgroundColor = .white
        self.view = view

        view.addSubview(tableView)
        tableView.translatesAutoresizingMaskIntoConstraints = false
        tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
        tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
        tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
        tableView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "CellID")

        diffableDataSource = UITableViewDiffableDataSource<Section, ViewModel>(tableView: tableView, cellProvider: { (tableView, indexPath, viewModel) -> UITableViewCell? in
            let cell = UITableViewCell(style: .subtitle, reuseIdentifier: "CellID")
            cell.textLabel?.text = viewModel.title
            cell.detailTextLabel?.text = viewModel.subtitle
            return cell
        })
        applySnapshot(models: items)

        let tgr = UITapGestureRecognizer(target: self, action: #selector(handleTap))
        view.addGestureRecognizer(tgr)
    }

    @objc func handleTap() {
        switch selectedItems {
        case .items:
            applySnapshot(models: items)
            selectedItems = .newItems
        case .newItems:
           applySnapshot(models: newItems)
           selectedItems = .items
        }
    }

    func applySnapshot(models: [ViewModel]) {
        var snapshot = NSDiffableDataSourceSnapshot<Section, ViewModel>()
        snapshot.appendSections([.main])
        snapshot.appendItems(models, toSection: .main)
        diffableDataSource?.apply(snapshot, animatingDifferences: true)
    }
}

enum Section {
    case main
}

struct ViewModel: Hashable {
    let id: Int
    let title: String
    let subtitle: String

    func hash(into hasher: inout Hasher) {
       hasher.combine(id)
    }
}


// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
like image 244
Angelus Avatar asked Jun 08 '20 16:06

Angelus


1 Answers

It's because you implemented Hashable incorrectly.

Remember, Hashable also means Equatable — and there is an inviolable relationship between the two. The rule is that two equal objects must have equal hash values. But in your ViewModel, "equal" involves comparing all three properties, id, title, and subtitle — even though hashValue does not, because you implemented hash.

In other words, if you implement hash, you must implement == to match it exactly:

struct ViewModel: Hashable {
    let id: Int
    let title: String
    let subtitle: String

    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
    static func ==(lhs: ViewModel, rhs: ViewModel) -> Bool {
        return lhs.id == rhs.id
    }
}

If you make that change, you'll find that the table view animation behaves as you expect.

If you also want the table view to pick up on the fact that the underlying data has in fact changed, then you also have to call reloadData:

    diffableDataSource?.apply(snapshot, animatingDifferences: true) {
        self.tableView.reloadData()
    }

(If you have some other reason for wanting ViewModel's Equatable to continue involving all three properties, then you need two types, one for use when performing equality comparisons plain and simple, and another for contexts where Hashable is involved, such as diffable data sources, sets, and dictionary keys.)

like image 86
matt Avatar answered Sep 22 '22 16:09

matt