How do you build an expandable outline with NSDiffableDataSourceSectionSnapshot?

7.12.2 · L3 · Interview

Predict first

What do levels, expansion and collapse do to the visible items?

import UIKit

struct Node: Hashable { let id: Int }
var ss = NSDiffableDataSourceSectionSnapshot<Node>()
let root = Node(id: 10), k1 = Node(id: 11), k2 = Node(id: 12), g = Node(id: 13)
ss.append([root])
ss.append([k1, k2], to: root)
ss.append([g], to: k1)
print(ss.items.count, ss.rootItems.map(\.id), ss.level(of: g), ss.parent(of: g)!.id, ss.visibleItems.count)
ss.expand([root])
print(ss.visibleItems.map(\.id), ss.isExpanded(root), ss.isExpanded(k1))
ss.expand([k1])
print(ss.visibleItems.map(\.id))
ss.collapse([root])
print(ss.visibleItems.map(\.id), ss.contains(g))
  • 4 [10] 2 11 1 / [10, 11, 12] true false / [10, 11, 13, 12] / [10] true
  • 4 [10] 2 11 4 / …
  • 4 [10, 11, 12, 13] 2 11 1 / …

Mental model. A section snapshot is a folder tree whose folders remember whether they are open; the visible list is just the open branches flattened.

A section snapshot models one section as a tree. You append(_:) root items, then append(_:to:) to add children under a parent, and the snapshot tracks parent(of:), level(of:), rootItems and expansion state. In the check, four nodes in three levels give level(of: g) = 2, a parent of 11, and a visibleItems count of 1 because everything starts collapsed; expanding the root shows its two children, expanding a child inserts its own child between it and its sibling, and collapsing the root hides the whole subtree while contains(g) stays true (collapsed items are still in the snapshot, just not visible).

You apply it with dataSource.apply(sectionSnapshot, to: section, animatingDifferences:), and the collection view's list layout shows outline disclosure through UICellAccessory.outlineDisclosure() on the cell; toggling expansion calls expand or collapse on a copy and re-applies it, or the data source's sectionSnapshotHandlers (willExpandItem, shouldCollapseItem, snapshotForExpandingParent) lazily load children. Expansion state lives in the snapshot, so persist the expanded ids yourself if the screen must restore. The same item identity rules apply: a child cannot appear twice in one data source, so a node that exists under two parents needs two distinct identifiers. Section snapshots work with the ordinary NSDiffableDataSourceSnapshot sections, and an outline in UIKit is a list layout, not a separate control (UIKit has no outline view as in AppKit).

Starts collapsed; expansion reveals children in tree order; collapsing hides but keeps them in the snapshot.

Common traps

  • Reusing one identifier under two parents. Identifiers must be unique across the data source.
  • Forgetting the disclosure accessory. Without it there is nothing to tap, even though the data is expandable.

They might follow up with

  • How do I load children lazily? Use snapshotForExpandingParent to supply them on expansion.
  • Is there a SwiftUI counterpart? List with children, or OutlineGroup.

Related questions

  • 7.12.1 An item's title changed but its cell still shows the old text after applying a new snapshot. Why, and what is…
  • 7.5.2 How does a diffable data source work, and what does it require of your item identifiers?

← An item's title changed but its cell still shows the old text after…   What does an orthogonally scrolling compositional section measure and… →