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()
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.)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With