Fixing SwiftData Scene Isolation in SwiftUI with the Memo Pattern
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
- True Scene Isolation: Each scene gets its own independent
ModelContextrather than sharing the globalcontainer.mainContext. - Survives Redraws: Unsaved user edits and local UI state persist smoothly when SwiftUI re-evaluates view bodies.
- No
.onChangeSide Effects: By handling state linkage directly inside the memoized property getter,you avoid asynchronous timing issues, layout flickers, and accidental update loops.