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:24)NSDiffableDataSourceSectionSnapshot 只泛型化 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 的内容。演讲用 recent、top、suggested 三个 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>
}
关键点:
shouldExpandItem和shouldCollapseItem决定用户操作是否允许继续。willExpandItem和willCollapseItem适合记录状态变化。snapshotForExpandingParent在 parent 即将展开时返回新的 section snapshot,适合按需加载昂贵的 children。- transcript 给出的例子是让某个 parent 永远不能 collapse,以及只在需要时加载昂贵内容。
用 transaction 提交拖拽排序
(08:52)Diffable Data Source 的重排序入口是 reorderingHandlers。演讲明确说,启用重排序需要同时提供 canReorderItem 和 didReorder。
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:45)NSDiffableDataSourceTransaction 提供变化前、变化后和差异信息。应用可以用这些信息推导新顺序。
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 的内容。怎么开始:先用
NSDiffableDataSourceSnapshotappend section 顺序,再循环为每个 section 创建并 applyNSDiffableDataSourceSectionSnapshot。 -
给 outline 做按需加载:只在用户展开父节点时加载子内容。为什么值得做:
snapshotForExpandingParent专门服务 expensive content,能减少初始 section snapshot 的内容量。怎么开始:设置dataSource.sectionSnapshotHandlers.snapshotForExpandingParent,在闭包里根据 parent 和当前 child snapshot 返回更新后的 section snapshot。 -
保存用户拖拽后的顺序:让列表支持用户重新排序,并把结果写回模型数组。为什么值得做:transcript 说明界面层可以自动提交 reordering change,但应用的 backing store 才是最终 source of truth。怎么开始:同时提供
canReorderItem和didReorder,在didReorder里读取NSDiffableDataSourceTransaction.difference或finalSnapshot,再更新本地数组。
关联 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