07961d8a2e57c3407617d76414d121ee
Data Source 新特性:基于 Diffable 实现局部刷新

WWDC 2019 Session 220:Advances in UI Data Sources
作者:@老峰

引言

在 iOS 开发中,UITableView 和 UICollectionView 是很常用的 UI 控件,在过去我们通常需要实现 Data Sources 来配置数据源,虽然在简单的业务中我们可以愉快的实现各种需求,可是一旦业务复杂起来,比如数据源实时的增删改,我们经常会一不小心就遇到 NSInternalInconsistencyException(Data Source 和当前 UI 状态不一致)等奇奇怪怪的异常,本文基于 WWDC 2019 Session 220:Advances in UI Data Sources 将从以下三部分分享此 Session 的脱水内容及作者的实践心得:

  • Data Source 使用现状
  • Diffable Data Source 新 API
  • Diffable Data Source 实践

1、Data Source 使用现状

这里以 Session 中 WiFi 设置为例,我们实现一个无线局域网列表页面如下图所示:

按照通常实现方式我们首先需要实现 UITableView 的 Data Source 方法

func numberOfSections(in tableView: UITableView) -> Int {
    return models.count
}
// Return the number of rows for the table.     
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
   return models[section].count
}

// Provide a cell object for each row.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
   // Fetch a cell of the appropriate type.
   let cell = tableView.dequeueReusableCell(withIdentifier: "cellTypeIdentifier", for: indexPath)

   // Configure the cell’s contents.
   cell.textLabel!.text = "WiFi text"

   return cell
}

由于每个网络可用网络的状态都在实时变化如网络 abert 路由器重启,新的网络 internal 被发现,这时我们需要移除 abert cell 这一行,加入 internal cell 这一行,最终实时状态如上图,那么我们按照以往的思路如何实现更新数据刷新 UI 状态呢?可能绝大多数同学会简单粗暴地 reloadData,事实上我们只有 2 条数据变更,如果在 TableView 较为复杂的时候可能全量刷新还会产生性能的问题,所以并不希望对 TableView 全量刷新,那么局部刷新又如何实现呢?

    tableView.beginUpdates()
    tableView.insertRows(at: indexPaths, with: .fade)
    tableView.deleteRows(at: indexPaths, with: .fade)
    self.tableView.endUpdates()

尽管如上代码可以实现需求,可是我们不得不计算需要插入或者删除的的 indexPaths ,而且稍有不慎我们将会遇到如下这个熟悉的异常:

*** Terminating app due to uncaught exception
'NSInternalInconsistencyException',
reason: 'Invalid update: invalid number of sections. The number of sections contained in the tableView view after the update (1) must be equal to the number of sections contained in the tableView view before the update (1), plus or minus the number of sections inserted or deleted (0 inserted, 1 deleted).'
***

事实上不管是 UITableView 还是 UICollectionView在我们调用 reloadSections 进行局部刷新时非常容易遇到 「NSInternalInconsistencyException(数据不一致)」的崩溃,如由于某些业务需要对 TableView 进行延时刷新极有可能出现 Data Source 和 TableView 中的 IndexPath 不一致出现异常,那么除了在业务代码中小心谨慎的 work around 规避此类问题,还有其他方式吗?有的接下来将介绍本文核心内容 Diffable Data Source 新 API。

2、Diffable Data Source 新 API

如上图所示在 iOS 13 中 Apple 引入了新的 API Diffable Data Source ,让开发者可以更简单高效的实现 UITableView、UICollectionView 的局部数据刷新。可能使用过 IGListKit 、RxCocoa 或者 DeepDiff 的读者对于 Diff 概念并不陌生,本文并不准备对 Diff 算法本身展开详细讨论,感兴趣的读者可自行查阅学习。

Note:在软件开发中 Diff 是一个很重要的概念,有很多应用场景,如 git 版本管理中文件变更应用;React中虚拟DOM 用 Diff算法更新 UI 状态;IGListKit 通过 IGListDiff 自动计算前后两次数据源的差值,实现局部数据刷新。

由于 UITableView 和 UICollectionView 的 API 大同小异,笔者 以 UITableView 为例讲解 Diffable Data Source,首先介绍 TableView 中 一个关键类 UITableViewDiffableDataSource, 其定义如下:

class UITableViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType> : NSObject where SectionIdentifierType : Hashable, ItemIdentifierType : Hashable

它是用来维护 TableView 的数据源,Section 和 Item 遵循 IdentifierType,从而确保每条数据的唯一性,初始化方法如下:

init(tableView: UITableView, cellProvider: @escaping UITableViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType>.CellProvider)

typealias UITableViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType>.CellProvider = (UITableView, IndexPath, ItemIdentifierType) -> UITableViewCell?

使用过 RxCocoa 的读者可能对 CellProvider 很眼熟,没错以后我们可以在初始化方法中配置 Cell,如果这里没看懂也没关系,我在下一小节将以实例具体介绍使用方法,在我们配置好 DiffableDataSource 后会通过本地缓存或网络请求刷新数据,如果涉及增删改则会有多次刷新,我们将使用如下代码对 TableView 进行刷新:

func apply(_ snapshot: NSDiffableDataSourceSnapshot<SectionIdentifierType, ItemIdentifierType>, animatingDifferences: Bool = true)

通过使用 apply 我们无需计算变更的 indexPaths,也无需调用 reloadSections,即可安全在在主线程或后台线程更新 UI, 仅需简单的将需要变更后的数据通过 NSDiffableDataSourceSnapshot 计算出来,NSDiffableDataSourceSnapshot 的定义如下:

```

top Created with Sketch.