swiftswiftuiswiftdata

How can I use a model from DocumentGroup alongside one from .modelContainer in Swift Data?


I’m building a document‑based Swift UI app using DocumentGroup with SwiftData. I want:

When I attach .modelContainer(for: User.self) to my ContentView, my Gate model (the document data) stops working. If I remove the User container, Gate works again but User is unavailable. How can I use both stores at the same time without the environments stepping on each other?

Minimal Example

Models

import SwiftData
import Foundation

// This should be stored inside the document 
@Model
final class Gate {
    var number: Int
    var manufacturer: String
    var year: Int
    var broken: Bool

    init(number: Int, manufacturer: String, year: Int, broken: Bool) {
        self.number = number
        self.manufacturer = manufacturer
        self.year = year
        self.broken = broken
    }
}

// This should be stored inside the app's container 
@Model
final class User {
    var name: String
    var locationCity: String
    var birthYear: Int

    init(name: String, locationCity: String, birthYear: Int) {
        self.name = name
        self.locationCity = locationCity
        self.birthYear = birthYear
    }
}

UTType for the document

import UniformTypeIdentifiers

extension UTType {
    static var gateDocument = UTType(exportedAs: "com.example.gatedoc")
}

App entry

import SwiftUI
import SwiftData

@main
struct MyApp: App {
    var body: some Scene {
        DocumentGroup(editing: Gate.self, contentType: .gateDocument) {
            // If I add the user container here, Gate stops working
            ContentView()
                .modelContainer(for: User.self)
        }
    }
}

My understanding is that View.modelContainer(for:) sets a new ModelContainer (and thus a new modelContext) in the environment for that view subtree, which overrides the ModelContainer provided by DocumentGroup. Since @Query uses the environment modelContext, there’s effectively only one active context per view subtree. I need to somehow get both models with their different configurations into one container.

I am aware that I can create a custom ModelContainer to get multiple objects into a single container (for storing only part of the Data in CloudKit for example). However since I am using DocumentGroup for Gate, I don’t know to control the behavior of its ModelContainer.


Solution

  • You can do something similar to my answer here, where you inject your own app-wide model container for User, into the environment.

    @main
    struct FooApp: App {
        var body: some Scene {
            DocumentGroup(editing: Gate.self, contentType: .gateDocument) {
                ContentView()
                    .environment(\.userContainer, Self.userContainer)
            }
        }
        
        static let userContainer = try! ModelContainer(for: User.self)
    }
    
    extension EnvironmentValues {
        @Entry var userContainer: ModelContainer? = nil
        
        @MainActor
        var userContext: ModelContext? {
            userContainer?.mainContext
        }
    }
    

    You can then switch between the two containers using the .modelContainer modifier. You can write a helper view like this that allows you to switch to a given ModelContainer within a @ViewBuilder closure:

    struct QueryView<Content: View, Model: PersistentModel>: View {
        
        @Query var models: [Model]
        let content: ([Model]) -> Content
        
        init(query: () -> Query<Model, [Model]>, content: @escaping ([Model]) -> Content) {
            self._models = query()
            self.content = content
        }
        
        var body: some View {
            content(models)
        }
    }
    
    struct QueryContainer<Content: View, Model: PersistentModel>: View {
        let content: ([Model]) -> Content
        let query: () -> Query<Model, [Model]>
        let container: ModelContainer
        
        init(_ container: ModelContainer, for type: Model.Type, query: @autoclosure @escaping () -> Query<Model, [Model]> = .init(), @ViewBuilder content: @escaping ([Model]) -> Content) {
            self.query = query
            self.content = content
            self.container = container
        }
        
        var body: some View {
            QueryView<Content, Model>(query: query, content: content)
                .modelContainer(container)
        }
    }
    

    Example usage: suppose you are in ContentView where the Gate model container is active:

    @Query
    var gates: [Gate] // You can directly query Gates
    
    @Environment(\.modelContext) var gateContext
    
    // you can get access to the container for Users like this
    @Environment(\.userContainer) var userContainer
    @Environment(\.userContext) var userContext
    
    var body: some View {
        GatesView(gates)
    
        // to also query users, wrap your view with QueryContainer 
        QueryContainer(userContainer, for: User.self) { users in 
            UsersView(users)
    
            // inside this closure, the Gates container is not active
            // if some view in here needs to query Gates, remember to set the model container back
            SomeViewThatQueriesGates()
                .modelContainer(gateContext.container) // access the container for Gates through the context for Gates
        }
    }
    

    Here is a complete ContentView where I have added forms to add gates and users, to demonstrate everything working

    struct ContentView: View {
        @Query
        var gates: [Gate]
        
        @Environment(\.modelContext) var gateContext
        @Environment(\.userContext) var userContext
        @Environment(\.userContainer) var userContainer
        
        @State private var number = 0
        @State private var manufacturer = ""
        @State private var year = 2000
        @State private var broken = false
        
        @State private var name = ""
        @State private var locationCity = ""
        @State private var birthYear = 1990
        
        var body: some View {
            VStack {
                HStack {
                    Form {
                        TextField("Number", value: $number, format: .number)
                        TextField("Manufacturer", text: $manufacturer)
                        TextField("Year", value: $year, format: .number)
                        Toggle("Broken", isOn: $broken)
                        Button("Add") {
                            gateContext.insert(Gate(number: number, manufacturer: manufacturer, year: year, broken: broken))
                            try! gateContext.save()
                        }
                    }
                    List(gates) {
                        Text($0.description)
                    }
                }
                HStack {
                    Form {
                        TextField("Name", text: $name)
                        TextField("Location City", text: $locationCity)
                        TextField("Birth Year", value: $birthYear, format: .number)
                        Button("Add") {
                            userContext?.insert(User(name: name, locationCity: locationCity, birthYear: birthYear))
                            try! userContext?.save()
                        }
                    }
                    QueryContainer(userContainer!, for: User.self) { users in
                        VStack {
                            List(users) {
                                Text($0.description)
                            }
                            SomeViewThatQueriesGates()
                                .modelContainer(gateContext.container)
                        }
                    }
                }
            }
        }
    }
    
    struct SomeViewThatQueriesGates: View {
        @Query
        var gates: [Gate]
        
        var body: some View {
            Text("\(gates.count) Gates")
        }
    }
    
    extension Gate: CustomStringConvertible {
        var description: String {
            "\(number): \(manufacturer) \(year), broken: \(broken)"
        }
    }
    
    extension User: CustomStringConvertible {
        var description: String {
            "\(name) in \(locationCity), born \(birthYear)"
        }
    }