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?
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!
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With