Why do Diffable Datasources treat class and struct types differently?
Asked Answered
E

1

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.

Epp answered 29/4, 2021 at 20:30 Comment(2)
Did you ever figure out? I've been stuck 2 days trying to figure this outEarlineearls
@Earlineearls I just stick to using structs 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.Epp
H
17

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.

Heall answered 16/11, 2021 at 18:56 Comment(4)
Very interesting, but how does it answer the question that was actually asked?Bertberta
@Bertberta I extended the answer. NSObjects's implementation of Hashable is forwarding to hash and isEqual: but not the other way round.Heall
@NickolayTarbayev Excellent answer, thank you very much. I'm interested in how you did your investigation.Epp
@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.Heall

© 2022 - 2024 — McMap. All rights reserved.