iosswiftdiffabledatasource

Why do Diffable Datasources treat class and struct types differently?


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.


Solution

  • 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.