Seeding Core Data
Seeding Core Data with a small amount of data turns out to be a much larger task than one would initially guess. There are several potential avenues for tackling this problem, such as parsing data from an XML or JSON file and saving it to a new persistent store, to procedurally generating the data however, these leave a bad taste in my mouth, as they aren’t very DRY. Not only that, but Apple explicitly discourage such practices, as some iOS devices don’t have the cycles to spare on such a task. A simpler approach could be to create the database in a simulator, embed it in the app bundle, and copy the resource into place on initial launch.
let modelName = "Model" // Same name as .xcdatamodeld directory guard let sqliteURL = Bundle.main.url(forResource: modelName, withExtension: "sqlite") else { fatalError("Unable to find sqlite database") } guard let shmURL = Bundle.main.url(forResource: modelName, withExtension: "sqlite-shm") else { fatalError("Unable to find shared memory file") } guard let walURL = Bundle.main.url(forResource: modelName, withExtension: "sqlite-wal") else { fatalError("Unable to find logging file") } let persistentContainerDirectoryURL = PersistentContainer.defaultDirectoryURL() let persistentContainerSQLiteURL = persistentContainerDirectoryURL.appendingPathComponent("\(modelName).sqlite") let persistentContainerShmUrl = persistentContainerDirectoryURL.appendingPathComponent("\(modelName).sqlite-shm") let persistentContainerWalUrl = persistentContainerDirectoryURL.appendingPathComponent("\(modelName).sqlite-wal") do { try FileManager.default.copyItem(at: sqliteURL, to: persistentContainerSQLiteURL) try FileManager.default.copyItem(at: shmURL, to: persistentContainerShmUrl) try FileManager.default.copyItem(at: walURL, to: persistentContainerWalUrl) } catch let error { print("\(error), \((error as NSError).userInfo)") }
While copyItems(at:to:)
will throw if the destination URL isn’t empty, it would be cleaner to wrap the above code in a block that only ever executes once. We can use UserDefaults to keep track of its state.
if UserDefaults.standard.bool(forKey: didLaunchBeforeKey) { ... UserDefaults.standard.set(true, forKey: didLaunchBeforeKey) }
But wait, you might ask, what about replacePersistentStore(at:destinationOptions:withPersistentStoreFrom:sourceOptions:ofType:)
? If your database is too big to be a resource in your app’s bundle, you might consider downloading the seed database on initial launch, in which case this would be preferred over manually copying files. Bundle resources, unfortunately, are read-only, which means the NSPersistentStore
instantiated from a bundle resource would be read-only as well.