Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 599
parapote Avatar asked Oct 11 '25 23:10

parapote


1 Answers

I had the exactly same issue. It was working perfectly fine in the simulator, but crashing on the device with very generic error.

Files in app bundle are read-only, so you cannot change nor create new ones in the bundle. But you want read-only access, right? Wrong. Apparently, that's not how it works by default.

If you open your default.store by some SQLite viewer, you can see it will automatically create default.store-shm and default.store-wal. You can find more details about those files in SQLite documentation.

If you run your app in Simulator, and open your bundle URL in Finder, you can see both of those files are created. That's fine when you run it on your computer, as you have access to the whole file system (or at least the part where your Bundle lives). That's not true when you run it on real device, with more strict access control.

Naïve solution

Now we see the problem. It sounds we just need to open that file in some folder with write access, so new files can be written there. Let's do that:

guard let storeUrl = Bundle.module.url(forResource: Self.databaseName, withExtension: nil) else {
    throw Error.missingFile
}

let fileManager = FileManager.default
let tempUrl = URL.temporaryDirectory.appending(component: Self.databaseName)
if !fileManager.fileExists(atPath: tempUrl.path) {
    do {
        try fileManager.copyItem(at: storeUrl, to: tempUrl)
    } catch {
        throw Error.copyIssue(error)
    }
}

let configuration = ModelConfiguration(url: tempUrl)
...

Now it is working, but it looks a bit dirty to copy whole database somewhere else just to access it. I wish there was better solution... wait a second, there is one:

Proper solution

It looks your database (and also mine) was misconfigured. Every SQLite database has configuration flags (pragmas), you can edit them in some SQLite browser. Journal mode is the one we are interesting about. In my case it was set to WALL, which was causing creation of unnecessary temp files. Once I set it to delete, it works like a charm.

That also allowed me to set ModelConfiguration correctly to read-only mode:

let configuration = ModelConfiguration(url: url, allowsSave: false)
like image 125
pleshis Avatar answered Oct 15 '25 06:10

pleshis