【问题标题】:Swift DiffableDataSource make insert&delete instead of reloadSwift Diffable Data Source 使插入删除而不是重新加载
【发布时间】:2020-09-27 17:23:33
【问题描述】:

我很难理解 DiffableDataSource 的工作原理。 我有这样的 ViewModel

struct ViewModel: Hashable {
  var id: Int
  var value: String

  func hash(into hasher: inout Hasher) {
     hasher.combine(id)
  }
}

我的 tableView 由 cachedItems 填充,例如上面的 ViewModele。当 API 响应到达时,我想添加一个新的,删除缺少的一个,刷新 tableView 中已经存在的项目的 viewModel.value 并最终订购它。 一切正常,除了一件事 - 重新加载项目。

我对 DiffableDataSource 的理解是,它比较 item.hash() 以检测项目是否已经存在,如果存在,那么如果 cachedItem != apiItem,它应该重新加载。 不幸的是,这不起作用,快照会删除和插入而不是重新加载。

DiffableDataSource 应该这样做吗?

当然,我有一个解决方案——为了让它工作,我需要遍历 cachedItems,当新项目包含相同的 id 时,我更新 cachedItem,然后我 applySnapshot 没有动画,然后我终于可以 applySnapshot 与动画删除/插入/排序动画。

但这个解决方案似乎更像是一个 hack,而不是一个有效的代码。有没有更清洁的方法来实现这一目标?

更新:

有显示问题的代码。它应该在操场上工作。 例如。 items 和 newItems 包含 id == 0 的 viewModel。 哈希是相同的,所以 diffableDataSource 应该重新加载,因为字幕不同。 但是有可见的删除/插入而不是重新加载


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()

【问题讨论】:

标签: swift diffabledatasource


【解决方案1】:

这是因为你错误地实现了 Hashable。

请记住,Hashable 也意味着 Equatable - 两者之间存在不可侵犯的关系。规则是两个相等的对象必须具有相等的哈希值。但是在您的 ViewModel 中,“相等”涉及比较所有三个属性,idtitlesubtitle — 即使 hashValue 没有,因为您实现了 hash

换句话说,如果你实现hash,你必须实现==才能完全匹配:

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
    }
}

如果您进行该更改,您会发现表格视图动画的行为符合您的预期。

如果您希望表格视图了解基础数据实际上已更改的事实,那么您还必须致电reloadData

    diffableDataSource?.apply(snapshot, animatingDifferences: true) {
        self.tableView.reloadData()
    }

(如果您有一些其他理由希望 ViewModel 的 Equatable 继续涉及所有三个属性,那么您需要 两种 类型,一种用于执行简单的相等比较和简单,另一个用于涉及 Hashable 的上下文,例如 diffable 数据源、集合和字典键。)

【讨论】:

  • 好的,但这并不能解决我的问题。当我添加“==”方法来比较id时,表格视图将找不到索引为0的项目中的字幕从“Subtitle2”更改为“New Subtitle2”。我认为 DiffableDataSource 处理所有更改。这是否意味着除了datasource.apply之前没有其他解决方案......我需要更新没有动画的currentItems然后应用带有动画的快照?
  • 好的,我明白你的意思了。恐怕我能做的最好的就是建议你也打电话给reloadData。当然会有延迟,但我不知道该怎么办。如果你想要自动动画,你只需要忍受它。
  • 酷。实际上,当我使用 DiffableDataSource 时,我从未想过可能会调用 reloadData。谢谢你。你让我开心。
  • “如果你实现哈希,你必须实现 == 来精确匹配它”——我不这么认为。如果== 比较三个属性(id、title 和 subtitle),而hash 只对其中一个(id)进行哈希处理,那么仍然满足“相等对象必须具有相同哈希值”的约定。
  • @BlazejSLEBODA:来自developer.apple.com/documentation/swift/hashable“两个相等的实例必须以相同的顺序将相同的值以 hash(into:) 的形式提供给 Hasher。”要求确定身份的所有值都用于散列值。 (另一方面,我不知道做出这种区分的充分理由。)
猜你喜欢
  • 2018-11-25
  • 2021-02-19
  • 1970-01-01
  • 1970-01-01
  • 2020-04-03
  • 1970-01-01
  • 2020-12-26
  • 2015-03-05
  • 1970-01-01
相关资源
最近更新 更多