WWDC Quick Look 💓 By SwiftGGTeam
Advances in diffable data sources

Advances in diffable data sources

观看原视频

Highlight

iOS 14 为 Diffable Data Source 增加 Section Snapshot 和 Reordering Handlers,让 UICollectionView 按 section 组合层级数据、自动处理 outline 展开折叠,并把用户拖拽后的顺序提交回应用数据源。

核心内容

Diffable Data Source 在 iOS 13 解决了 collection view 和 table view 更新时的一个老问题:开发者先描述当前 UI 状态,再让框架计算差异和动画。这个模型适合扁平列表,但 Emoji Explorer 这类页面更复杂。它同时有横向滚动的 emoji 网格、可展开的 outline 区域,以及看起来像 table view 的列表区域。

iOS 14 把更新粒度拆到 section。Section Snapshot 表示单个 UICollectionView section 的内容,数据源可以由多个 section snapshot 组合出来。横向网格、outline、列表 section 都能各自维护自己的内容,再放进同一个 collection view。

层级数据是这场演讲的重点。Section Snapshot 的 append(_:to:) 用可选 parent 参数建立父子关系,expand(_:)collapse(_:) 管理展开状态。配合新的 outline disclosure accessory,用户点开或收起项目时,框架会更新 section snapshot 并重新应用到 data source。

另一个新增点是拖拽排序。Diffable Data Source 已经用唯一 item identifier 表示 UI 状态,iOS 14 继续提供 reorderingHandlers 和 transaction。用户拖动完成后,应用拿到变化前后的 snapshot、CollectionDifference 和每个 section 的 transaction,再把新顺序写回自己的 backing store。

详细内容

用 Section Snapshot 表示一个 section

03:24NSDiffableDataSourceSectionSnapshot 只泛型化 item identifier,没有 section identifier。它不关心自己属于哪个 section,只描述一组 item 以及它们之间的层级关系。

public struct NSDiffableDataSourceSectionSnapshot<ItemIdentifierType> where ItemIdentifierType : Hashable {
    public init()
    public mutating func append(_ items: [ItemIdentifierType], to parent: ItemIdentifierType? = nil)
    public mutating func expand(_ items: [ItemIdentifierType])
    public mutating func collapse(_ items: [ItemIdentifierType])
    public func isExpanded(_ item: ItemIdentifierType) -> Bool
    public func parent(of child: ItemIdentifierType) -> ItemIdentifierType?
    public var rootItems: [ItemIdentifierType] { get }
    public var visibleItems: [ItemIdentifierType] { get }
}

关键点:

  • ItemIdentifierType 必须遵守 Hashable,因为 diffable data source 依赖唯一 item identifier。
  • append(_:to:) 不传 parent 时,把 item 放到根层级。
  • expand(_:)collapse(_:) 修改 section snapshot 的展开状态。
  • visibleItems 只返回当前展开状态下可见的 item,适合调试 outline 显示结果。

先排 section,再填每个 section 的内容

04:43)普通 snapshot 负责 section 顺序,section snapshot 负责每个 section 的内容。演讲用 recenttopsuggested 三个 section 演示了这个分工。

func update(animated: Bool=true) {

   // Add our sections in a specific order
   let sections: [Section] = [.recent, .top, .suggested]
   var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
   snapshot.appendSections(sections)
   dataSource.apply(snapshot, animatingDifferences: animated)

   // update each section's data via section snapshots in the existing position
   for section in sections {
      let sectionItems = items(for: section)
      var sectionSnapshot = NSDiffableDataSourceSectionSnapshot<Item>()
      sectionSnapshot.append(sectionItems)
      dataSource.apply(sectionSnapshot, to: section, animatingDifferences:animated)
   }
}

关键点:

  • NSDiffableDataSourceSnapshot<Section, Item> 仍然用来确定 section 的整体顺序。
  • 每个 section 都创建自己的 NSDiffableDataSourceSectionSnapshot<Item>
  • dataSource.apply(sectionSnapshot, to: section, ...) 把局部内容应用到指定 section。
  • 这种写法让多个 section 互不干扰,适合一个页面同时有网格、outline 和列表。

建立 outline 的父子关系

05:18)层级数据通过同一个 append API 建立。先 append 根节点,再把子节点 append 到指定 parent。

// Create hierarchical data for our Outline

var sectionSnapshot = ...

sectionSnapshot.append(["Smileys", "Nature",
                        "Food", "Activities",
                        "Travel", "Objects", "Symbols"])

sectionSnapshot.append(["🥃", "🍎", "🍑"], to: "Food")

关键点:

  • 第一段 append 没有 parent,所以这些字符串都是根节点。
  • 第二段 append 指定 to: "Food",三个 emoji 成为 Food 的子节点。
  • transcript 明确说明,这样就能得到一个描述应用层级数据的 section snapshot。

06:01)需要单独处理某个 parent 的 children 时,可以从 section snapshot 里取子 snapshot。

let childSnapshot = sectionSnapshot.snapshot(for: parent, includingParent: false)

关键点:

  • snapshot(for:includingParent:) 只取指定 parent 下面的部分层级。
  • includingParent: false 表示结果里不包含 parent 自身。
  • 这个 API 适合只刷新或检查 outline 的局部数据。

控制展开、收起和懒加载

06:11)展开状态也是 section snapshot 的状态。修改后要重新 apply 到 data source,界面才会更新。

struct NSDiffableDataSourceSectionSnapshot<Item: Hashable> {
   func expand(_ items: [Item])
   func collapse(_ items: [Item])
   func isExpanded(_ item: Item) -> Bool
}

关键点:

  • expand(_:) 设置指定 item 的展开状态。
  • collapse(_:) 设置指定 item 的收起状态。
  • isExpanded(_:) 用于判断当前 item 是否已经展开。
  • 逐字稿提醒:如果只 mutate section snapshot,不 apply 到 Diffable Data Source,变化不会显示到界面上。

07:21)用户点击 outline disclosure accessory 后,框架会更新 section snapshot。需要拦截、观察或懒加载时,用 sectionSnapshotHandlers

extension UICollectionViewDiffableDataSource {

  struct SectionSnapshotHandlers<Item> {
    var shouldExpandItem: ((Item) -> Bool)?
    var willExpandItem: ((Item) -> Void)?

    var shouldCollapseItem: ((Item) -> Bool)?
    var willCollapseItem: ((Item) -> Void)?

    var snapshotForExpandingParent: ((Item, NSDiffableDataSourceSectionSnapshot<Item>) -> NSDiffableDataSourceSectionSnapshot<Item>)?
  }

  var sectionSnapshotHandlers: SectionSnapshotHandlers<Item>

}

关键点:

  • shouldExpandItemshouldCollapseItem 决定用户操作是否允许继续。
  • willExpandItemwillCollapseItem 适合记录状态变化。
  • snapshotForExpandingParent 在 parent 即将展开时返回新的 section snapshot,适合按需加载昂贵的 children。
  • transcript 给出的例子是让某个 parent 永远不能 collapse,以及只在需要时加载昂贵内容。

用 transaction 提交拖拽排序

08:52)Diffable Data Source 的重排序入口是 reorderingHandlers。演讲明确说,启用重排序需要同时提供 canReorderItemdidReorder

extension UICollectionViewDiffableDataSource {

  struct ReorderingHandlers {
    var canReorderItem: ((Item) -> Bool)?
    var willReorder: ((NSDiffableDataSourceTransaction<Section, Item>) -> Void)?
    var didReorder: ((NSDiffableDataSourceTransaction<Section, Item>) -> Void)?
  }

  var reorderingHandlers: ReorderingHandlers
}

关键点:

  • canReorderItem 在用户尝试开始拖拽时调用,返回 true 才允许拖拽。
  • didReorder 在拖拽完成后调用,用来把新顺序写回应用的数据源。
  • transcript 说明,框架能基于唯一 item identifier 自动提交界面层的 reordering change,但应用仍要更新 backing store。

09:45NSDiffableDataSourceTransaction 提供变化前、变化后和差异信息。应用可以用这些信息推导新顺序。

struct NSDiffableDataSourceTransaction<Section, Item> {
   var initialSnapshot: NSDiffableDataSourceSnapshot<Section, Item> { get }
   var finalSnapshot: NSDiffableDataSourceSnapshot<Section, Item> { get }
   var difference: CollectionDifference<Item> { get }
   var sectionTransactions: [NSDiffableDataSourceSectionTransaction<Section, Item>] { get }
}

struct NSDiffableDataSourceSectionTransaction<Section, Item> {
   var sectionIdentifier: Section { get }
   var initialSnapshot: NSDiffableDataSourceSectionSnapshot<Item> { get }
   var finalSnapshot: NSDiffableDataSourceSectionSnapshot<Item> { get }
   var difference: CollectionDifference<Item> { get }
}

关键点:

  • initialSnapshot 是更新前的 data source 状态。
  • finalSnapshot 是更新后的 data source 状态。
  • difference 是 Swift 标准库的 CollectionDifference
  • sectionTransactions 提供每个受影响 section 的局部变化。
  • transcript 说明,如果应用的 source of truth 是 Array,可以把 CollectionDifference 直接应用到这个数组。

核心启发

  • 做一个可展开的分类浏览器:把文件夹、书签分类或商品类目放进 outline。为什么值得做:session 明确展示 section snapshot 能表达父子层级,并由 outline disclosure accessory 驱动展开状态。怎么开始:用 append(rootItems) 放根节点,再用 append(children, to: parent) 建立子节点,最后把 section snapshot apply 到对应 section。

  • 把复杂首页拆成多个独立 section:例如最近使用、热门项目、推荐内容各自维护一个 section snapshot。为什么值得做:演讲中的 Emoji Explorer 就由三个 distinct section snapshots 组成,每个 snapshot 表示单个 section 的内容。怎么开始:先用 NSDiffableDataSourceSnapshot append section 顺序,再循环为每个 section 创建并 apply NSDiffableDataSourceSectionSnapshot

  • 给 outline 做按需加载:只在用户展开父节点时加载子内容。为什么值得做:snapshotForExpandingParent 专门服务 expensive content,能减少初始 section snapshot 的内容量。怎么开始:设置 dataSource.sectionSnapshotHandlers.snapshotForExpandingParent,在闭包里根据 parent 和当前 child snapshot 返回更新后的 section snapshot。

  • 保存用户拖拽后的顺序:让列表支持用户重新排序,并把结果写回模型数组。为什么值得做:transcript 说明界面层可以自动提交 reordering change,但应用的 backing store 才是最终 source of truth。怎么开始:同时提供 canReorderItemdidReorder,在 didReorder 里读取 NSDiffableDataSourceTransaction.differencefinalSnapshot,再更新本地数组。

关联 Session

  • Advances in UICollectionView — 继续看 iOS 14 的 collection view 更新,包括 section snapshots、lists、cell registration 和现代配置方式。
  • Lists in UICollectionView — 讲列表和 sidebar 如何建立在 UICollectionView 与 compositional layout 上,适合接着落地本 session 的 outline 数据。
  • Modern cell configuration — 补齐 collection view 和 table view cell 的 content/background configuration,用来配置列表和 outline 里的 cell。
  • Stacks, Grids, and Outlines in SwiftUI — 从 SwiftUI 角度看层级数据、outline 和列表展示,可以与 UIKit 的 section snapshot 对照阅读。

评论

GitHub Issues · utterances