Add App Group to Existing SwiftData App

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
    ])
}
Answered by DTS Engineer in 844884022

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:

  1. 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.

  2. 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.

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:

  1. 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.

  2. 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.

test

You don't need to define that in your @Model class.

Instead, in the main app, you have:

let container: ModelContainer
    
init() {
    let schema = Schema([YourModelName.self]) // Your model name inside
    let modelConfiguration = ModelConfiguration(schema: schema, allowsSave: true)
        
    do {
        container = try ModelContainer(for: schema, configurations: [modelConfiguration])
        print("Model Container created successfully.")
    } catch {
        fatalError("Could not create Model Container: \(error.localizedDescription)")
    }
}

Then in your widget, in the timeline provider declare the variable for the model container:

var container: ModelContainer = {
    try! ModelContainer(for: YourModelName.self)
}()

Then you need to just define your getEntries() function that returns an array of entries accordingly

var descriptor = FetchDescriptor<YourModelName>
let allEntries = try?container.mainContext.fetch(descriptor)
return allEntries ?? [] // Empty array if it throws

Don't forget to add the App Groups under the Signing & Capabilities tab for both the main app target and widget extension. Make sure it is the same one, e.g. group.com.yourcompany.YourAppName in both. Build it and make sure it is white and not in red for the App Groups.

It sounds like option 2 is the one I will need to do. Are there examples on how to do this? I'm unsure of how to locate the existing store and how to copy it to the App Group container's root folder and couldn't find anything from searches

You can retrieve the store URL with the following code:

yourModelContainer.configurations.first?.url

By default, SwiftData uses the .applicationSupportDirectory folder in the current user domain as the parent folder to create the data store, which you can retrieve with the following code:

FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first)

A SwiftData data store can contain multiple files, and even hidden files. Assuming you use the default data store (DefaultStore), I'd suggest that you use replacePersistentStore(at:destinationOptions:withPersistentStoreFrom:sourceOptions:type:), which is a Core Data API, to copy the store.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

Add App Group to Existing SwiftData App
 
 
Q