Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 338
shallowThought Avatar asked Oct 02 '16 16:10

shallowThought


3 Answers

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.

like image 65
Tom Harrington Avatar answered Nov 07 '22 07:11

Tom Harrington


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)")
    }
}
like image 22
blwinters Avatar answered Nov 07 '22 08:11

blwinters


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

like image 2
David.Chu.ca Avatar answered Nov 07 '22 08:11

David.Chu.ca