Fixing SwiftData Scene Isolation in SwiftUI with the Memo Pattern

Published by malhal on

When building multi-scene iOS or macOS applications with SwiftData, one subtle trap catches many developers off guard: SwiftUI’s .modelContainer(_:) modifier automatically injects container.mainContextdirectly into the environment.

If you have multiple windows or scenes, they all end up sharing that exact same main context. Tweaking properties like autosaveEnabled = false on one scene inadvertently changes it globally, and resetting your views can cause SwiftUI to wipe out uncommitted work.

To achieve true scene isolation while keeping your views clean and reactive, you can combine a simple generic Memo pattern with a custom view modifier.

The Memo Pattern

Instead of fighting SwiftUI’s struct re-instantiation or relying on asynchronous .onChange modifiers that trigger after the fact, we can use a lightweight generic class to cache expensive reference types as long as their input remains identical.

final class Memo<Input: Equatable, Value> {
    private var cached: (input: Input, value: Value)?

    func value(for input: Input, make: (Input) -> Value) -> Value {
        if let cached, cached.input == input {
            return cached.value
        }
        let value = make(input)
        cached = (input, value)
        return value
    }
}

Creating the Scene-Isolated Modifier

By pairing Memo with @State, we can instantiate a dedicated, non-main ModelContext per scene container.Because the context is memoized, unsaved changes survive view redraws, while environment-bound properties (like the undoManager) stay perfectly synchronized.

import SwiftUI
import SwiftData

struct SceneModelContextModifier: ViewModifier {
    let container: ModelContainer

    @Environment(\.undoManager) private var undoManager

    // Memoizes the ModelContext to survive redraws as long as the container is identical.
    @State private var modelContextMemo = Memo<ModelContainer, ModelContext>()

    func body(content: Content) -> some View {
        content
            .modelContext(modelContext)
    }

    private var modelContext: ModelContext {
        // Create the context once per container, disabling global autosave
        let mc = modelContextMemo.value(for: container) { container in
            let context = ModelContext(container)
            context.autosaveEnabled = false
            return context
        }
        
        // Link dependent environment state idempotently during evaluation
        if mc.undoManager !== undoManager {
            mc.undoManager = undoManager
        }
        
        return mc
    }
}

extension View {
    func sceneModelContainer(_ container: ModelContainer) -> some View {
        modifier(SceneModelContextModifier(container: container))
    }
}

Why This Approach Shines

  1. True Scene Isolation: Each scene gets its own independent ModelContext rather than sharing the global container.mainContext.
  2. Survives Redraws: Unsaved user edits and local UI state persist smoothly when SwiftUI re-evaluates view bodies.
  3. No .onChange Side Effects: By handling state linkage directly inside the memoized property getter,you avoid asynchronous timing issues, layout flickers, and accidental update loops.