'How to set a custom store URL for NSPersistentContainer

How can I set a custom store.sqlite URL to NSPersistentContainer?

I have found an ugly way, subclassing NSPersistentContainer:

final public class PersistentContainer: NSPersistentContainer {
private static var customUrl: URL?

public init(name: String, managedObjectModel model: NSManagedObjectModel, customStoreDirectory baseUrl:URL?) {
    super.init(name: name, managedObjectModel: model)
    PersistentContainer.customUrl = baseUrl
}

override public class func defaultDirectoryURL() -> URL {
    return (customUrl != nil) ? customUrl! : super.defaultDirectoryURL()
}

}

Is there a nice way?

Background: I need to save to an App Groups shared directory.



Solution 1:[1]

You do this with the NSPersistentStoreDescription class. It has an initializer which you can use to provide a file URL where the persistent store file should go.

let description = NSPersistentStoreDescription(url: myURL)

Then, use NSPersistentContainer's persistentStoreDescriptions attribute to tell it to use this custom location.

container.persistentStoreDescriptions = [description]

Note: myURL must provide the complete /path/to/model.sqlite, even if it does not exist yet. It will not work to set the parent directory only.

Solution 2:[2]

Expanding on Tom's answer, when you use NSPersistentStoreDescription for any purpose, be sure to init with NSPersistentStoreDescription(url:) because in my experience if you use the basic initializer NSPersistentStoreDescription() and loadPersistentStores() based on that description, it will overwrite the existing persistent store and all its data the next time you build and run. Here's the code I use for setting the URL and description:

let container = NSPersistentContainer(name: "MyApp")
            
let storeDirectory = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
// or
let storeDirectory = NSPersistentContainer.defaultDirectoryURL()

let url = storeDirectory.appendingPathComponent("MyApp.sqlite")
let description = NSPersistentStoreDescription(url: url)
description.shouldInferMappingModelAutomatically = true
description.shouldMigrateStoreAutomatically = true
container.persistentStoreDescriptions = [description]
    
container.loadPersistentStores { (storeDescription, error) in
    if let error = error as? NSError {
        print("Unresolved error: \(error), \(error.userInfo)")
    }
}

Solution 3:[3]

I just find out that the location of db created by PersistentContainer is different from db created by UIManagedDocument. Here is a snapshot of db location by UIManagedDocument:

enter image description here

and the following codes are used to create the db:

let fileURL = db.fileURL // url to ".../Documents/defaultDatabase"
let fileExist = FileManager.default.fileExists(atPath: fileURL.path)
if fileExist {
    let state = db.documentState
    if state.contains(UIDocumentState.closed) {
        db.open()
    }
} else {
    // Create database
    db.save(to: fileURL, for:.forCreating)
}

It looks like that the db referred by PersistentContainer is actually the file further down under folder "StoreContent" as "persistentStore"

This may explain why the db "defaultDatabase" in my case cannot be created by PersistentContainer if you want to specify your customized db file, or causing crash since folder already existed. I further verified this by appending a file name "MyDb.sqlite" like this:

let url = db.fileURL.appendingPathComponent("MyDb.sqlite")
let storeDesription = NSPersistentStoreDescription(url: url)
container.persistentStoreDescriptions = [storeDesription]
print("store description \(container.persistentStoreDescriptions)"
// store description [<NSPersistentStoreDescription: 0x60000005cc50> (type: SQLite, url: file:///Users/.../Documents/defaultDatabase/MyDb.sqlite)
container.loadPersistentStores() { ... }

Here is the new MyDb.sqlite:

enter image description here

Based on the above analysis, if you have codes like this:

if #available(iOS 10.0, *) {
    // load db by using PersistentContainer
    ...
 } else {
    // Fallback on UIManagedDocument method to load db
    ...
 }

Users' device may be on iOS pre 10.0 and later be updated to 10+. For this change, I think that the url has to be adjusted to avoid either crash or creating a new(empty) db (losing data).

Solution 4:[4]

This is the code that I use to initialize a pre-populated sqlite db that works consistently. Assuming you will use this db as read only then there is no need to copy it to the Documents dir on the device.

let repoName = "MyPrepopulatedDB"
let container = NSPersistentContainer(name: repoName)
let urlStr = Bundle.main.path(forResource: "MyPrepopulatedDB", ofType: "sqlite")
let url = URL(fileURLWithPath: urlStr!)
let persistentStoreDescription = NSPersistentStoreDescription(url: url)
persistentStoreDescription.setOption(NSString("true"), forKey: NSReadOnlyPersistentStoreOption)
container.persistentStoreDescriptions = [persistentStoreDescription]

container.loadPersistentStores(completionHandler: { description, error in
  if let error = error {
    os_log("ERROR: Failed to initialize persistent store, error is \(error.localizedDescription)")
  } else {
    os_log("Successfully loaded persistent store, \(description)")
  }
})

Some very important steps/items to keep in mind:

  • when constructing the URL to the sqlite file use the URL(fileURLWithPath:) form of the initializer. It seems that core data requires file based URLs, otherwise you will get an error.
  • I used a unit test to run some code in order to create/pre-populate the db in the simulator.
  • I located the full path to the sqlite file by adding a print statement inside the completion block of loadPersistentStores(). The description parameter of this block contains the full path to the sqlite file.
  • Then using Finder you can copy/paste that file in the app project.
  • At the same location as the sqlite file there are two other files (.sqlite-shm & .sqlite-wal). Add these two to the project also (in the same directory as the sqlite file). Without them core data throws an error.
  • Set the NSReadOnlyPersistentStoreOption in persistentStoreDescription (as shown above). Without this you get a warning (possible future fatal error).

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 shallowThought
Solution 2 malhal
Solution 3
Solution 4