9

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.

Bryan Bryce
  • 1,310
  • 1
  • 16
  • 29
  • Did you ever figure out? I've been stuck 2 days trying to figure this out – gmogames Sep 23 '21 at 00:48
  • @gmogames I just stick to using `struct`s whenever possible. If the data can be identical for two structs when I add an `let id: UUID` to each struct so that they are always unique. If you get two structs with the same data the app will crash. If the data needs to be updated I create a new struct with the same id but change the values and add that to the new snapshot. – Bryan Bryce Sep 23 '21 at 23:31

1 Answers1

14

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.

  • Very interesting, but how does it answer the question that was actually asked? – matt Nov 16 '21 at 20:24
  • @matt I extended the answer. `NSObjects`'s implementation of `Hashable` is forwarding to `hash` and `isEqual:` but not the other way round. – Nickolay Tarbayev Nov 17 '21 at 10:46
  • @NickolayTarbayev Excellent answer, thank you very much. I'm interested in how you did your investigation. – Bryan Bryce Nov 18 '21 at 19:25
  • 1
    @BryanBryce I just set a couple of breakpoints for the struct items case and dug through the assembler code in the call stack looking for some meaningful symbols. There I found the `NSOrederedSet` and `_bridgeAnythingToObjectiveC` function. Then I verified my assumption by implementing Obj-C methods in a Swift class used as an item. – Nickolay Tarbayev Nov 19 '21 at 09:29