Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use two Stores/Configurations in SwiftData

I am trying to create a SwiftData app that has two separate SQLite data stores - one for data I will pre-create and distribute with my app and one for data the user will create when running the app - something like described in this article by Paul Hudson.

When I create and save items I get the error:

Failed to save after place creation: The model configuration used to open the store is incompatible with the one that was used to create the store.

For testing, I have two simple models:

@Model
class Person: Identifiable {
    @Attribute(.unique) let id: UUID
    let name: String
    
    init(id: UUID = UUID(), name: String) {
        self.id = id
        self.name = name
    }
}

@Model
class Place: Identifiable {
    @Attribute(.unique) let id: UUID
    let name: String
    
    init(id: UUID = UUID(), name: String) {
        self.id = id
        self.name = name
    }
}

I initialise SwiftData:

@main
struct MultiStoreApp: App {
    
    var container: ModelContainer
    
    init() {
        let peopleStoreURL = URL.documentsDirectory.appending(path: "people.store")
        let placeStoreURL = URL.documentsDirectory.appending(path: "places.store")
        
        let peopleConfig = ModelConfiguration(schema: Schema([Person.self]), url: peopleStoreURL)
        let placesConfig = ModelConfiguration(schema: Schema([Place.self]), url: placeStoreURL)
        
        do {
            self.container = try ModelContainer(for: Person.self, Place.self, configurations: peopleConfig, placesConfig)
        } catch {
            fatalError("Failed to create model container: \(error.localizedDescription)")
        }
    }
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .modelContainer(container)
        }
    }
}

I then have a simple View that displays a list of all places and people, along with buttons to add people and places.

struct ContentView: View {
    @Environment(\.modelContext) private var modelContext
    
    @Query var people: [Person]
    @Query var places: [Place]
    
    var body: some View {
        NavigationStack {
            List {
                Section("People") {
                    ForEach(people) { person in
                        VStack(alignment: .leading) {
                            Text(person.name)
                            Text(person.id.uuidString)
                                .font(.caption)
                        }
                    }
                }
                
                Section("Places") {
                    ForEach(places) { place in
                        VStack(alignment: .leading) {
                            Text(place.name)
                            Text(place.id.uuidString)
                                .font(.caption)
                        }
                    }
                }
            }
            
            GroupBox {
                HStack {
                    Button(action: {
                        let person = Person(name: "Person \(people.count)")
                        modelContext.insert(person)
                        do {
                            try modelContext.save()
                        } catch {
                            print("Failed to save after person creation: \(error.localizedDescription)")
                        }
                    }, label: {
                        Text("Add Person")
                    })
                    
                    Spacer()
                    
                    Button(action: {
                        let place = Place(name: "Place \(places.count)")
                        modelContext.insert(place)
                        do {
                            try modelContext.save()
                        } catch {
                            print("Failed to save after place creation: \(error.localizedDescription)")
                        }
                    }, label: {
                        Text("Add Place")
                    })
                }
            }
            .padding()
        }
    }
}

When I run the app on a clean simulator, I can add objects of one type, but as soon as I add an object of the other type I start getting errors.

Is what I'm trying to do possible?

like image 265
Keith Sharp Avatar asked Oct 29 '25 10:10

Keith Sharp


1 Answers

This has been a problem since the betas (See 1, 2). The error message changed from back then, but it appears that they still haven't completely fixed it.

I'd just use two containers. Inject one of the containers as a new Environment value. Here I have done this with Place.

let container: ModelContainer
let placesContainer: ModelContainer

init() {
    let peopleStoreURL = URL.documentsDirectory.appending(path: "people.store")
    let placeStoreURL = URL.documentsDirectory.appending(path: "places.store")
    
    let peopleConfig = ModelConfiguration(schema: Schema([Person.self]), url: peopleStoreURL)
    let placesConfig = ModelConfiguration(schema: Schema([Place.self]), url: placeStoreURL)
    do {
        self.container = try ModelContainer(for: Person.self, configurations: peopleConfig)
        self.placesContainer = try ModelContainer(for: Place.self, configurations: placesConfig)
    } catch {
        fatalError("Failed to create model container: \(error.localizedDescription)")
    }
}

var body: some Scene {
    WindowGroup {
        ContentView()
    }
    .modelContainer(container)
    .placesContainer(placesContainer)
}
struct PlacesContainer: EnvironmentKey {
    static let defaultValue: ModelContainer = try! .init()
}

extension EnvironmentValues {
    var placesContainer: ModelContainer {
        get { self[PlacesContainer.self] }
        set { self[PlacesContainer.self] = newValue }
    }
}

extension Scene {
    func placesContainer(_ container: ModelContainer) -> some Scene {
        environment(\.placesContainer, container)
    }
}

extension View {
    @MainActor func placesContainer(_ container: ModelContainer) -> some View {
        environment(\.placesContainer, container)
    }
}

You cannot put two @Querys that query different containers in the same view (the query always queries the "current" model context). So you need a separate view to do the query, and set the modelContainer of that view to be the placesContainer.

struct SeparateView: View {
    @Query var places: [Place]
    var body: some View {
        // ...
    }
}

// in ContentView...
@Environment(\.placesContainer) var placesContainer

var body: some View {
    SeparateView().modelContainer(placesContainer)
}

I have written a helper view for doing this:

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)
    }
}

Then ContentView can look like this:

@Environment(\.modelContext) private var modelContext
@Environment(\.placesContainer) private var placesContainer
@Query var people: [Person]

var body: some View {
    QueryContainer(placesContainer, for: Place.self) { places in
        NavigationStack {
            // ...
            // remember to change the "insert places" button action 
            // to insert into the correct container!

            // all these subviews here will have the places container
            // if any of the subviews views need the people container, remember to set it!
        }
    }
}
like image 151
Sweeper Avatar answered Nov 01 '25 12:11

Sweeper