iOS DiffableDataSource的使用

7,894 阅读3分钟

在WWDC2019,苹果推出了一种在UITableView和UICollectionView使用的全新数据源的设置方式:UITableViewDiffableDataSourceUICollectionViewDiffableDataSource,支持iOS13及以上版本使用。

先来看看我们用了10年的dataSource是如何设置的:

  • 设置section数量;
  • 设置item数量;
  • 设置相应的cell;

image.png

这样设置dataSource经常会出现像下面所示的crash:

image.png

遇到这种问题的时候,通常是因为在操作indexPath和数据源的过程中导致的数组越界,有时候debug起来并不简单。经常你会通过调用reloadData()解决问题,但是reloadData()不带动画,这就降低了用户体验。而且有时候你并不希望刷新整个视图。

接下来看看DiffableDataSource的用法

因为DiffableDataSource在TableView和CollectionView的用法类似,所以下面用TableView来演示。

  • 首先先了解一个关键的东西:NSDiffableDataSourceSnapshot:我们可以理解它是一个快照,我们需要给这个快照设置相应的section和item,并将此快照apply到dataSource上,后续可以修改快照的内容,改变显示的结果。

    image.png

    image.png

  • 再来看看苹果怎么定义UITableViewDiffableDataSource的:

    image.png 这里看得出SectionIdentifierTypeItemIdentifierType都是需要遵从Hashable的,目的是确保唯一性。

  • 我们先定义一个遵从HashableSectionIdentifierTypeItemIdentifierType

    // 枚举默认就是Hashable类型
    enum Section: CaseIterable {
        case main
    }
    
    // 自定义类型需要遵从Hashable协议
    struct MyModel: Hashable {
        var title: String
    
        init(title: String) {
            self.title = title
            self.identifier = UUID()
        }
    
        private let identifier: UUID
    
        func hash(into hasher: inout Hasher) {
            hasher.combine(self.identifier)
        }
    }
    
  • 定义使用SectionMyModel的dataSource:

    private var dataSource: UITableViewDiffableDataSource<Section, MyModel>! = nil
    
  • 设置tableView的UI、dataSource,生成snapShot并将它用到dataSource上:

    // 这是即将用来展示的models
    private lazy var mainModels = [MyModel(title: "Item1"),
                                   MyModel(title: "Item2"),
                                   MyModel(title: "Item3")]
    
    // tableView
    private lazy var tableView: UITableView = {
        let tableView = UITableView(frame: .zero, style: .insetGrouped)
        tableView.translatesAutoresizingMaskIntoConstraints = false
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "identifier")
        tableView.delegate = self
        return tableView
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupTableView() // 设置tableView UI
        setupDataSource() // 设置tableView的dataSource
        applySnapshot() // 给dataSource设置snapshot
    }
    
    func setupTableView() {
        view.addSubview(tableView)
        NSLayoutConstraint.activate([
            tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            tableView.topAnchor.constraint(equalTo: view.topAnchor),
            tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
    }
    
    func setupDataSource() {
        dataSource = UITableViewDiffableDataSource<Section, MyModel>.init(tableView: self.tableView, cellProvider: {(tableView, indexPath, model) -> UITableViewCell? in
            let cell = tableView.dequeueReusableCell(withIdentifier: "identifier", for: indexPath)
            var content = cell.defaultContentConfiguration()
            content.text = model.title
            cell.contentConfiguration = content
            return cell
        })
        dataSource.defaultRowAnimation = .fade
    }
    
    func applySnapshot() {
        // 往snapshot里插入sections和items
        var snapshot = NSDiffableDataSourceSnapshot<Section, MyModel>()
        snapshot.appendSections([.main])
        snapshot.appendItems(mainModels, toSection: .main)
        dataSource.apply(snapshot, animatingDifferences: true)
    }
    

    运行效果如下:

    image.png

  • 当我们需要修改数据,比如点击了就删掉该行,可以用2种方式实现,个人觉得第2种比较方便一点,不用手动操作snapshot中的item:

    • 方法1(不推荐,代码容易出错):
      • snapshot()取到当前的snapshot;
      • 删掉snapshot中对应的item;
      • 删掉mainModels中的model;
      • 将改变后的snapshot运用到dataSource上;
    extension ViewController: UITableViewDelegate {
        func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
            // 取到当前的snapshot
            var snapshot = dataSource.snapshot()
            // 删掉snapshot中对应的item
            let item = mainModels[indexPath.row]
            snapshot.deleteItems([item])
            // 删掉mainModels中的model
            mainItems.remove(at: indexPath.row)
            // 将改变后的snapshot运用到dataSource上
            dataSource.apply(snapshot, animatingDifferences: true)
        }
    }
    
    • 方法2:
      • 删掉mainModels中的model;
      • 生成一个新的空snapshot;
      • 重新将section、item等数据加到snapshot中;
      • 将新的snapshot运用到dataSource上去;
    extension ViewController: UITableViewDelegate {
        func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
            mainItems.remove(at: indexPath.row)
            applySnapshot()
        }
    }
    

    运行效果:
    demo.gif

参考资料: