swiftswift-data

How to pre-populate an app with a read-only store located in the app's bundle?


I'm trying to preload an app with read-only data contained in a default.store file located in the app's bundle.

import SwiftUI
import SwiftData

@main
struct AppDemo: App {
    enum Schema: VersionedSchema {
        static var versionIdentifier: SwiftData.Schema.Version = .init(1, 0, 0)
        static var models: [any PersistentModel.Type] = [Item.self]
    }
    
    let container: ModelContainer

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(container)
    }
    
    init() {
        do {
            let url = Bundle.main.url(forResource: "default", withExtension: "store")!
            let schema = SwiftData.Schema(versionedSchema: Schema.self)
            let configuration = ModelConfiguration(url: url)
            container = try ModelContainer(for: schema, configurations: configuration)
        } catch {
            debugPrint(error)
        }
    }
}

I've used the approach shown in this example.

For reasons I don't know, it works perfectly in the simulator (iOS 17.0). But the same code fails when run on a device (iOS 17.0, still).

Unresolved error loading container Error Domain=NSCocoaErrorDomain Code=134110 "An error occurred during persistent store migration." UserInfo={sourceURL=file:///private/var/containers/Bundle/Application/[UUID]/AppDemo.app/default.store, reason=Cannot migrate store in-place: error during SQL execution : attempt to write a readonly database, destinationURL=file:///private/var/containers/Bundle/Application/[UUID]/AppDemo.app/default.store, NSUnderlyingError=0x281c7ae20 {Error Domain=NSCocoaErrorDomain Code=134110 "An error occurred during persistent store migration." UserInfo={NSSQLiteErrorDomain=8, NSFilePath=/private/var/containers/Bundle/Application/[UUID]/AppDemo.app/default.store, NSUnderlyingException=error during SQL execution : attempt to write a readonly database, reason=error during SQL execution : attempt to write a readonly database}}}

CoreData: Attempt to add read-only file at path file:///private/var/containers/Bundle/Application/.../AppDemo.app/default.store read/write. Adding it read-only instead. This will be a hard error in the future; you must specify the NSReadOnlyPersistentStoreOption.

How to configure the ModelContainer as read-only?


Solution

  • The error message indicates that the CoreData is attempting to add a read-only file as read/write. This results in a failure because the file is within your application's bundle, which is read-only. The application's bundle cannot be modified once the app has been installed, so any attempt to write to this location will fail.

    You need to copy the default.store file from the bundle to the application's Documents directory (which is read/write) during the first launch of the app. Then, you can use this copied file for your ModelContainer.

    init() {
        do {
            guard let bundleURL = Bundle.main.url(forResource: "default", withExtension: "store") else {
                fatalError("Failed to find default.store in app bundle")
            }
    
            let fileManager = FileManager.default
            let documentDirectoryURL = try fileManager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
            let documentURL = documentDirectoryURL.appendingPathComponent("default.store")
    
            // Only copy the store from the bundle to the Documents directory if it doesn't exist
            if !fileManager.fileExists(atPath: documentURL.path) {
                try fileManager.copyItem(at: bundleURL, to: documentURL)
            }
    
            let schema = SwiftData.Schema(versionedSchema: Schema.self)
            let configuration = ModelConfiguration(url: documentURL)
            container = try ModelContainer(for: schema, configurations: configuration)
        } catch {
            debugPrint(error)
        }
    }
    

    W first get the URL of the default.store file in the app bundle. Then, we get the URL of the Documents directory and append "default.store" to it to get the destination URL. If the default.store file doesn't already exist in the Documents directory, we copy it from the bundle to the Documents directory. Finally, we use the default.store file in the Documents directory to create the ModelContainer.