Updating collection views using diffable data sources
At a glance
| Item | Summary |
|---|---|
| Purpose | Drive a three-column recipe interface with stable identifiers, diffable snapshots, and a mutable backing store. |
| App architecture | RecipeSplitViewController owns selected collection/recipe state and coordinates sidebar, list, and detail columns; DataStore owns recipe mutations, while sidebar and list controllers render independent diffable snapshots keyed by stable values and Recipe.ID. |
| Main patterns | Split-view state coordinator, Identifier-based snapshots, Notification-driven store updates |
| Project style | Nine Swift files; storyboard-composed split view with model, store, sidebar, list, detail, and notification roles. |
Project structure
Source bundle/
├── DiffableDataSourceSample/
│ ├── AppDelegate.swift
│ ├── SidebarViewController.swift
│ ├── RecipeSplitViewController.swift
│ ├── RecipeListViewController.swift
│ ├── RecipeDetailViewController.swift
│ ├── SceneDelegate.swift
│ ├── Models/
│ │ ├── Data.swift
│ │ └── Recipe.swift
│ └── NotificationName.swift
├── Configuration/
│ └── SampleCode.xcconfig
└── DiffableDataSourceSample.xcodeproj/
├── .xcodesamplecode.plist
└── project.pbxproj
Structure observations
- The split controller is the navigation/state coordinator; it does not own recipe persistence data.
- Snapshots contain stable identifiers, and cell providers resolve full models from
DataStoreat render time. - Sidebar section snapshots and recipe-list snapshots are separate read models for the same store-driven feature.
Overall architecture
flowchart LR
STORE["DataStore recipes and collections"] --> SIDEBAR["SidebarViewController"]
SIDEBAR --> FILTER["RecipeSplitViewController.selectedRecipes"]
FILTER --> NOTICE["selectedRecipesDidChange IDs"]
NOTICE --> LIST["RecipeListViewController snapshot"]
LIST --> SELECT["selectedRecipeId"]
SELECT --> SPLIT["RecipeSplitViewController"]
SPLIT --> DETAIL["RecipeDetailViewController"]
STORE --> EVENTS["add/change/delete notifications"]
EVENTS --> SPLIT
EVENTS --> LIST
Reference code
DiffableDataSourceSample/RecipeSplitViewController.swift:45 — split-owned selection state
var selectedRecipes: SelectedRecipes? {
didSet {
guard let navController = viewController(for: .supplementary) as? UINavigationController,
let recipeList = navController.topViewController as? RecipeListViewController
else { return }
recipeList.showRecipes()
}
}
var selectedRecipeId: Recipe.ID? {
didSet {
if let id = selectedRecipeId {
showRecipeDetail(with: id)
} else {
hideRecipeDetail()
show(.supplementary)
}
}
}Interpretation
The split controller carries cross-column selection, while each child owns its own collection-view projection. Store events invalidate those projections through notifications. Stable IDs cross the boundaries; full Recipe values are resolved only where cell/detail content needs them.
Ownership and state
classDiagram
GlobalDataStore *-- DataStore : let dataStore
DataStore *-- RecipeArray : allRecipes
DataStore *-- StringArray : collections
RecipeSplitViewController *-- SelectedRecipes : selection state
RecipeListViewController *-- DiffableDataSource : private
SidebarViewController *-- DiffableDataSource : private
RecipeSplitViewController o-- RecipeListViewController : storyboard child
RecipeSplitViewController o-- RecipeDetailViewController : storyboard child
ImageStore *-- UIImageDictionary : cache
Ownership evidence
DiffableDataSourceSample/RecipeListViewController.swift:14 — list-owned projection state
private enum RecipeListSection: Int {
case main
}
private var recipeListDataSource:
UICollectionViewDiffableDataSource<RecipeListSection, Recipe.ID>!
private var recipeSplitViewController: RecipeSplitViewController {
self.splitViewController as! RecipeSplitViewController
}| Owner | Object or state | Relationship | Mutation authority |
|---|---|---|---|
Global dataStore reference |
One DataStore instance loaded from bundled JSON |
Creates the store once for the app module | DataStore methods are the recipe/collection mutation authority. |
RecipeSplitViewController |
Selected recipe group and selected recipe ID | Stores cross-column navigation state | Its observers show/hide list/detail content and publish selected IDs. |
RecipeListViewController |
Recipe-list diffable source | Creates and privately retains the projection | It applies ID snapshots and maps selection back to the split coordinator. |
SidebarViewController |
Sidebar item types and diffable source | Owns private navigation projection state | It creates hierarchical section snapshots and changes selected recipe groups. |
ImageStore.shared |
Decoded image cache | Static singleton retains the dictionary | Only the store guarantees and inserts cached images. |
Composition denotes source-visible construction and retained value state. Aggregation denotes a stored or injected collaborator whose exclusive lifetime the source does not prove.
Class and protocol design
DiffableDataSourceSample/RecipeListViewController.swift:250 — identifier-to-model cell provider
recipeListDataSource = UICollectionViewDiffableDataSource(
collectionView: collectionView) { collectionView, indexPath, identifier in
// The snapshot stores Recipe.ID, not Recipe.
let recipe = dataStore.recipe(with: identifier)!
return collectionView.dequeueConfiguredReusableCell(
using: recipeCellRegistration,
for: indexPath,
item: recipe)
}| Type | Responsibility | Depends on or conforms to |
|---|---|---|
RecipeSplitViewController |
Coordinates selected filter and detail identity across columns | UISplitViewController |
SidebarViewController |
Builds hierarchical filter/collection navigation | UICollectionViewController with private diffable item types |
RecipeListViewController |
Projects selected IDs and handles list mutations/selection | UICollectionViewController |
RecipeDetailViewController |
Displays and edits one selected recipe | UIViewController |
DataStore / ImageStore |
Own mutable recipes/collections and decoded images | Concrete store classes |
Recipe / SelectedRecipes |
Stable domain identity and filter query | Value types |
Collection and split behavior use UIKit callbacks. The sample defines no local repository or coordinator protocol; controllers read the concrete module-level dataStore, so substitutability is not an architectural goal here.
Access control
| Symbol | Access | Verified effect | Design reason |
|---|---|---|---|
| List/sidebar item types and data sources | private |
Visible only inside their controller’s lexical scope | Snapshot schema and UIKit projection remain controller implementation details. |
ImageStore.images, scale, _guaranteeImage |
fileprivate |
Accessible anywhere in Models/Data.swift, but nowhere else |
Same-file model helpers can share cache internals without making them module-wide. |
DataStore.collection and updateCollectionsIfNeeded |
fileprivate |
Same-file store implementation only | Collection derivation is not a controller-facing command. |
| Store/controller/domain types | implicit internal |
Visible throughout the app module | The sample is not a reusable public library. |
Reference code
DiffableDataSourceSample/Models/Data.swift:41 — same-file image-cache boundary
final class ImageStore {
typealias _ImageDictionary = [String: UIImage]
fileprivate var images: _ImageDictionary = [:]
fileprivate static var scale = 2
static var shared = ImageStore()
fileprivate func _guaranteeImage(name: String) -> _ImageDictionary.Index {
if let index = images.index(forKey: name) { return index }
images[name] = ImageStore.loadImage(name: name)
return images.index(forKey: name)!
}
}fileprivate is deliberately wider than private but narrower than internal: helpers elsewhere in Data.swift may participate in cache/store implementation. No public or open feature API appears.
Logic ownership and placement
| Logic | Owning type or file | Why it lives there |
|---|---|---|
| Cross-column selected filter and detail | RecipeSplitViewController |
It can see all split roles and decides which child to show. |
| Recipe mutation and collection derivation | DataStore |
It owns canonical arrays and emits mutation notifications. |
| Recipe ID snapshot and list actions | RecipeListViewController |
They are one projection of selected store data. |
| Hierarchical navigation snapshots | SidebarViewController |
Sidebar-specific item/section types do not leak into other columns. |
| Image decoding/cache | ImageStore |
Keeps heavyweight asset retrieval out of cells and recipes. |
Design patterns
| Pattern | Source evidence | Purpose or tradeoff |
|---|---|---|
| Split-view state coordinator | DiffableDataSourceSample/RecipeSplitViewController.swift:43 |
Centralizes only state that crosses sidebar, list, and detail columns. |
| Identifier-based snapshots | DiffableDataSourceSample/RecipeListViewController.swift:78 |
Lets the diffable source track identity independently of mutable model values. |
| Notification-driven store updates | DiffableDataSourceSample/Models/Data.swift:148 |
Invalidates interested projections without direct controller references. |
Naming conventions
- Controller names map to split roles (
Sidebar,RecipeList,RecipeDetail,RecipeSplit). - Selection state distinguishes a query (
selectedRecipes) from one identity (selectedRecipeId). - Snapshot/item types include their projection role (
RecipeListSection,SidebarItem). - Private cache helpers use an underscore to mark low-level guarantee semantics within
Data.swift.
Architecture takeaways
- Put only cross-column selection on the split coordinator; keep each diffable projection private to its controller.
- Snapshot stable identifiers and resolve mutable models from the authoritative store.
- Use
fileprivatenarrowly when several same-file declarations truly share an implementation boundary.
Source map
| Source file | Architectural role |
|---|---|
DiffableDataSourceSample/RecipeSplitViewController.swift |
Cross-column selection coordination |
DiffableDataSourceSample/SidebarViewController.swift |
Hierarchical sidebar diffable projection |
DiffableDataSourceSample/RecipeListViewController.swift |
Recipe-ID snapshots, cells, selection, and list actions |
DiffableDataSourceSample/RecipeDetailViewController.swift |
Selected-recipe presentation and editing |
DiffableDataSourceSample/Models/Data.swift |
Recipe store, notifications, and image cache |
DiffableDataSourceSample/Models/Recipe.swift |
Recipe domain model and stable ID |