Sample CodeiOS, iPadOS, Mac CatalystReviewed 2026-07-21View on Apple Developer

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 DataStore at render time.
  • Sidebar section snapshots and recipe-list snapshots are separate read models for the same store-driven feature.

Overall architecture

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

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 fileprivate narrowly 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