I work on a MacOS app (which has a companion iOS app) that uses Core Data with NSPersistentCloudKitContainer
.
The app also supports widgets and hence there is a need to migrate the persistent store within the core data stack using replacePersistentStore( at:...
.
During development I recently created a new model version and added a new entity, replaced some attributes etc...
Working on the iOS app is fine because deleting the app clears all the data allowing me to work with a clean slate.
On MacOS, I initially thought that I could simply navigate to the app group file location, delete the .sqlite
file, along with the sqlite-shm
and sqlite-wal
.
I also went and deleted the CloudKit related files.
I did all of this out of pure ignorance - my expectation was that it would give me a clean slate, but it did not.
This instead gave me some unpredictable behaviour, but the app was always in a bad state. the issues I saw were;
• migration failure
,
• sqlite errors highlighting no such column: t0
- where all the new entity details were missing in sqlite completely
After finding a post in the forums about how to reset macOS correctly, I did this instead -
do {
try container.persistentStoreCoordinator.destroyPersistentStore(at: container.persistentStoreDescriptions.first!.url!, type: .sqlite, options: nil)
try container.persistentStoreCoordinator.destroyPersistentStore(at: storeURL, type: .sqlite, options: nil)
} catch {
print(String(describing: error))
}
And now I am back to the ongoing error of This NSPersistentStoreCoordinator has no persistent stores (schema mismatch or migration failure). It cannot perform a save operation.
Another thing to note - whenever running the destroyPersistentStore(
I have tried this on both the URLs of the old store location and the new one (in the app group). This still doesn't seem to help. AND I noticed that while destroyPersistentStore
does get rid of the .sqlite file, it does not delete the sqlite-shm
and sqlite-wal
- could this be the problem? and do I need to delete these manually?
public class CoreDataManager {
public static let shared = CoreDataManager()
private enum Constants {
#if os(macOS)
static let appGroupName = "2MM2V2959F.wabitime.group"
#elseif os(iOS)
static let appGroupName = "group.wabitime"
#endif
static let containerName = "WabiTimeDataModel"
/// The name of the sql database file
static let databaseName = "wabitime_database"
/// The identifier for the container
static let containerIdentifier = "iCloud.com.jslomowitz.WabiTime"
}
public lazy var context = persistentContainer.viewContext
lazy var managedObjectModel: NSManagedObjectModel = {
guard
let wabiDataBundle = Bundle.module.url(
forResource: Constants.containerName,
withExtension: "momd"
),
let managedObjectModel = NSManagedObjectModel(contentsOf: wabiDataBundle)
else {
assertionFailure("cannot find managedObjectModel")
return NSManagedObjectModel()
}
return managedObjectModel
}()
lazy var persistentContainer: NSPersistentCloudKitContainer = {
let container = NSPersistentCloudKitContainer(
name: Constants.containerName,
managedObjectModel: managedObjectModel
)
/// URL of the old sql database that has not been relocated to the app group
var oldStoreURL: URL? {
guard
let storeDescription = container.persistentStoreDescriptions.first,
let url = storeDescription.url,
FileManager.default.fileExists(atPath: url.path)
else {
return nil
}
return url
}
/// URL of the sql database in the app group
var storeURL: URL {
guard let fileContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: Constants.appGroupName) else {
fatalError("Shared file container could not be created")
}
return fileContainer.appendingPathComponent("\(Constants.databaseName).sqlite")
}
// assign the shared container if the old store has been deleted
if oldStoreURL == nil {
let description = NSPersistentStoreDescription(url: storeURL)
description.shouldInferMappingModelAutomatically = true
description.shouldMigrateStoreAutomatically = true
container.persistentStoreDescriptions = [description]
}
// perform store migration if necessary
if let url = oldStoreURL, url.absoluteString != storeURL.absoluteString {
let coordinator = container.persistentStoreCoordinator
do {
let storeOptions = [
NSMigratePersistentStoresAutomaticallyOption: true,
NSInferMappingModelAutomaticallyOption: true
]
try coordinator.replacePersistentStore(
at: url,
withPersistentStoreFrom: storeURL,
sourceOptions: storeOptions,
type: .sqlite
)
} catch {
print(error.localizedDescription)
}
self.deleteOldStore(with: url)
}
let options = NSPersistentCloudKitContainerOptions(containerIdentifier: Constants.containerIdentifier)
guard let description = container.persistentStoreDescriptions.first else {
fatalError("Could not retrieve a persistent store description.")
}
description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
description.cloudKitContainerOptions = options
container.loadPersistentStores(completionHandler: { [weak self] (_, error) in
guard let self, error == nil else {
assertionFailure("Unresolved error: \(String(describing: error))")
return
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergePolicy(merge: .mergeByPropertyObjectTrumpMergePolicyType)
return container
}()
private func deleteOldStore(with url: URL) {
let fileCoordinator = NSFileCoordinator()
fileCoordinator.coordinate(writingItemAt: url, options: .forDeleting, error: nil) { url in
do {
try FileManager.default.removeItem(at: url)
} catch {
print(error.localizedDescription)
}
}
}
// MARK: - Core Data Saving and Undo support
func saveContext(completion: (() -> Void)? = nil) {
#if os(macOS)
if !context.commitEditing() {
NSLog("AppDelegate unable to commit editing before saving")
}
#endif
if context.hasChanges {
do {
try context.save()
print("SAVED")
completion?()
} catch {
let nserror = error as NSError
#if os(macOS)
NSApplication.shared.presentError(nserror)
#endif
}
}
}
}