Advances in diffable data sources
Swift & UI 进阶 26m

Diffable Data Source 的进阶用法

Advances in diffable data sources

2020年6月26日

在 Apple 官方观看视频

一句话判断

iOS 14 为 Diffable Data Source 新增了 Section Snapshot 和 Reordering 支持——如果你还在用传统的 performBatchUpdates 管理列表数据变化,是时候升级了。

这场 Session 讲了什么

Diffable Data Source 在 iOS 13 引入,用快照(Snapshot)替代了传统的 insert/delete/reload/move 操作。iOS 14 带来了两个重要增强:Section Snapshot(按 Section 管理数据)和 Item-based Reordering(拖拽排序支持)。

Section Snapshot 允许你独立管理每个 Section 的数据层次。这在树形结构展示中特别有用——比如文件浏览器中每个文件夹就是一个 Section,可以独立展开和折叠。NSDiffableDataSourceSectionSnapshot 提供了树形数据的增删改操作。

Reordering 支持让 Diffable Data Source 可以与 UICollectionView 的 Drag & Drop 集成。通过 canReorderItemreorderingHandlers 回调,你可以让用户拖拽排序列表项,数据源会自动更新 Snapshot。

值得深挖的点

Section Snapshot 与普通 Snapshot 的区别。 普通 NSDiffableDataSourceSnapshot 是扁平的——所有 Item 在同一层级。Section Snapshot 支持树形层次(parent-child 关系),可以递归地表示嵌套数据。对于大纲视图、文件浏览器这类需求,Section Snapshot 是更自然的选择。

Reordering 的数据一致性保证。 拖拽排序时,Diffable Data Source 的 reorderingHandlers.didReorder 回调给你一个 NSDiffableDataSourceTransaction,包含变更前后的 Snapshot。你可以在回调中将新的顺序同步到数据模型中,确保数据源和模型始终一致。

代码片段

import UIKit

// 配置支持拖拽排序的 Diffable Data Source
let dataSource = UICollectionViewDiffableDataSource<Section, Item>(
    collectionView: collectionView
) { collectionView, indexPath, item in
    let cell = collectionView.dequeueReusableCell(
        withReuseIdentifier: "Cell", for: indexPath
    ) as! UICollectionViewListCell
    var content = cell.defaultContentConfiguration()
    content.text = item.title
    cell.contentConfiguration = content
    return cell
}

// 启用拖拽排序
dataSource.reorderingHandlers.canReorderItem = { item in
    return true  // 所有项都可以拖拽排序
}

dataSource.reorderingHandlers.didReorder = { transaction in
    // 获取排序后的新数据顺序
    let updatedItems = transaction.finalSnapshot.itemIdentifiers
    // 将新顺序同步到你的数据模型中
    saveNewOrder(updatedItems)
}
// Section Snapshot——管理树形数据
func updateOutlineView(with files: [FileItem]) {
    var sectionSnapshot = NSDiffableDataSourceSectionSnapshot<FileItem>()
    
    func addChildren(_ items: [FileItem], to parent: FileItem?) {
        for item in items {
            if let children = item.children {
                // 有子项:递归添加
                sectionSnapshot.append([item], to: parent)
                addChildren(children, to: item)
                // 默认折叠
                sectionSnapshot.collapse([item])
            } else {
                // 叶子节点:直接添加
                sectionSnapshot.append([item], to: parent)
            }
        }
    }
    
    addChildren(files, to: nil)
    dataSource.apply(sectionSnapshot, to: .main, animatingDifferences: true)
}
// 展开/折叠处理
// 在 Item 上监听展开折叠事件
class OutlineCell: UICollectionViewListCell {
    override func systemLayoutSizeFitting(
        _ targetSize: CGSize,
        withHorizontalFittingPriority horizontalPriority: UILayoutPriority,
        verticalFittingPriority: UILayoutPriority
    ) -> CGSize {
        // 使用 self.configurationState 判断是否展开
        return super.systemLayoutSizeFitting(
            targetSize,
            withHorizontalFittingPriority: horizontalPriority,
            verticalFittingPriority: verticalFittingPriority
        )
    }
}

最佳实践

  • 新项目直接用 Diffable Data Source。 不要再用 performBatchUpdates,Diffable 方式更安全、更简洁、更不容易出 bug。
  • 树形数据用 Section Snapshot,扁平数据用普通 Snapshot。 两者的 API 略有不同,选对了用起来更顺手。
  • 拖拽排序后立即同步数据模型。 didReorder 回调中拿到新顺序后,务必更新你的持久化数据,否则下次刷新会恢复旧顺序。
  • 利用 Snapshot 的差异化更新。 apply 时设置 animatingDifferences: true,系统会自动计算数据变化并添加动画。

还有什么值得关注

  • Section Snapshot 支持 expand/collapse 动画,自动处理树形结构的展开收起
  • Diffable Data Source 的 Snapshot 支持 reconfigureItems 方法,iOS 15 会进一步扩展这个能力
  • UICollectionView 的 isPrefetching 可以配合 Diffable Data Source 实现预加载
  • Section Snapshot 的 isVisible 方法可以检查某个 Item 当前是否在可见区域
WWDC 2020