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
}
}
}
}
Another question that comes up is that over the course of 4 years we have on occasion had MacOS users that complain that their app keeps crashing and they can't use it anymore, even if they delete the app entirely from their system. We have tried offering the advice to delete residual app files (sqlite and the other CloudKit ones which seem to stick around)
For some users, this seems to work and others it doesn't work at all. What do we do about this?
And in the case of complete migration failure, is there a way to rebuild the database, and how would we go about doing that?