I have an existing app that uses SwiftData and now want to add widgets. I added the widget extension, created an App Group to use for the main app target and widget targets and successfully created the widget. However, when testing the updates I often experience data loss - as though the including the widget extension is creating a new instance of modelContainer. Am I missing something to ensure there won't be any data loss when adding the App Group and widget extension?
For additional context: I’ve followed the Backyard Birds example code except that it uses a separate app package. My app does not use an external app package, but I am using some elements of the DataGeneration file. My files containing the SwiftData models have Target Memberships for both the main app target and widget extension target.
In the TimelineProvider for my widgets, I'm doing the following:
let modelContext = ModelContext(DataGeneration.container)
init() {
DataGeneration.generateAllData(modelContext: modelContext)
}
My DataGeneration file (simplified) is as follows. When adding the widget target, I sometimes see the log for "Creating instance of DataGeneration".
import Foundation
import SwiftData
@Model
class DataGeneration {
var requiresInitialization: Bool = true
init(requiresInitialization: Bool = true) {
self.requiresInitialization = requiresInitialization
}
private func generateInitialData(modelContext: ModelContext) {
if requiresInitialization {
let budget = Budget()
modelContext.insert(budget)
requiresInitialization = false
}
}
private static func instance(with modelContext: ModelContext) -> DataGeneration {
if let result = try! modelContext.fetch(FetchDescriptor<DataGeneration>()).first {
logger.info("Found instance of DataGeneration")
return result
} else {
logger.info("Creating instance of DataGeneration")
let instance = DataGeneration()
modelContext.insert(instance)
return instance
}
}
static func generateAllData(modelContext: ModelContext) {
let instance = instance(with: modelContext)
instance.generateInitialData(modelContext: modelContext)
}
}
extension DataGeneration {
static let container = try! ModelContainer(for: schema, configurations: [.init(isStoredInMemoryOnly: DataGenerationOptions.inMemoryPersistence)])
static let schema = SwiftData.Schema([
DataGeneration.self,
Budget.self
])
}
When you create a model container using a model configuration without a store URL, SwiftData automatically looks into the entitlements of your app, and if finding an App Group container, uses the container URL (containerURL(forSecurityApplicationGroupIdentifier:)) as the parent folder to create the data store. I believe this's why your app doesn't load the existing store after you add the App Group container.
To use the existing store in your case, consider the following options:
-
Specify the URL of the existing store for the model configuration used to create your model container. This way, the store isn't in the App Group container, and hence can't be shared with your widget.
-
Copy the existing store to the App Group container's root folder so SwiftData finds and loads it when creating the model container.
If you would share the data store between your main app and widget, option 2 will be your choice.
Regarding the following:
as though the including the widget extension is creating a new instance of modelContainer
Your widget extension runs in a separate process, and so does need to create a model container instance, which is different from the one your main app creates, to be able to access SwiftData.
Best,
——
Ziqiao Chen
Worldwide Developer Relations.