Diffable datasources require specifying a SectionIdentifierType
and an ItemIdentifierType
and these types have to conform to Hashable
Supposedly they must conform to Hashable
so that the datasource can do its diffing.
So why does it behave differently depending on if the identifier type is a class or a struct even when the == and hash functions are the same? Or even the === function is overridden for classes so that it acts more like a value type?
Example:
import UIKit
public class DebugViewController: UIViewController {
typealias SectionType = IntWrapper
typealias ItemType = IntWrapper
public class IntWrapper: Hashable {
public static func == (lhs: DebugViewController.IntWrapper, rhs: DebugViewController.IntWrapper) -> Bool {
lhs.number == rhs.number
}
public static func === (lhs: DebugViewController.IntWrapper, rhs: DebugViewController.IntWrapper) -> Bool {
lhs.number == rhs.number
}
public func hash(into hasher: inout Hasher) {
hasher.combine(number)
}
var number: Int
init(number: Int) {
self.number = number
}
}
private var dataSource: UITableViewDiffableDataSource<SectionType, ItemType>!
@IBOutlet var tableView: UITableView!
public override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "DefaultCell")
dataSource = UITableViewDiffableDataSource<SectionType, ItemType>(tableView: tableView) { (tableView, indexPath, item) -> UITableViewCell? in
let cell = tableView.dequeueReusableCell(withIdentifier: "DefaultCell")!
cell.textLabel?.text = "\(item.number)"
return cell
}
apply()
}
@IBAction func buttonTapped(_ sender: Any) {
apply()
}
func apply() {
var snapshot = NSDiffableDataSourceSnapshot<SectionType, ItemType>()
let sections = [IntWrapper(number: 0)]
let items = [IntWrapper(number: 1)]
snapshot.appendSections(sections)
sections.forEach { snapshot.appendItems( items, toSection: $0) }
dataSource.apply(snapshot, animatingDifferences: true)
}
}
If IntWrapper
is a struct the table view does nothing when apply()
is called (apply()
essentially loads in the same data) For me, this is the expected behavior.
If IntWrapper
is a class the table view reloads when apply() is called. Also, the hash()
and == functions are NOT even called.
I don't think this can be answered unless someone has access to the source (hint, hint) or unless I made some mistake in my example.
After some investigation I found that UITableViewDiffableDataSource
uses NSOrderedSet
under the hood. Before passing the array of identifiers to the ordered set it is being converted to an array of Objective-C objects (by means of Swift._bridgeAnythingToObjectiveC<τ_0_0>(τ_0_0) -> Swift.AnyObject
function). Because Swift and Objective-C classes share same memory layout they are passed as is. NSOrderedSet
then relies on the hash
and isEqual:
Objective-C methods instead of Hashable
, and Swift provides default implementations for those same as for NSObject
even when a class is not subclassed from NSObject
, but there's no forwarding calls to Hashable
(only the other way round).
That said, the only correct way of using classes in diffable data sources is to subclass them from NSObject
or at least implement hash()
and isEqual(_:)
methods with @objc
annotation.