简体   繁体   中英

Core Data. How to swap NSPersistentStores and inform NSFetchedResultsController?

I'm implementing backup and restore (via Dropbox) of a user's Core Data persisted data. For a restore I pull the files down from Dropbox and store them temporarily in the Documents directory. Then I create a new NSPersistentContainer and use it to replace the current persistent store (in the ApplicationSupport directory) before deleting the no longer needed files in the Documents directory.

I'm using the MasterDetail template for now and so I have the usual time-stamped entities and the NSFetchedResultsController that comes along with that. Having made the replacement I swap back to the master tableView and unfortunately see the original set of entities. When the app restarts I see the restored data as intended but need to work out how to make the NSFetchedResultsController see the new restored data automatically.

This is my restore code:

func restorePSFromBackup() {
    // Current persistent store is in the ApplicationSupport directory.
    let currentPSFolderUrl = FileManager.default.urls(for: .applicationSupportDirectory, in:.userDomainMask).first!
    // The current Core Data file (url).
    let currentPSUrl1 = currentPSFolderUrl.appendingPathComponent("DBCDTest.sqlite")
    // Backup persistent store with which to restore is in the Documents directory.
    let backUpFolderUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
    // The 3 backup Core Data files (urls).
    let backupUrl1 = backUpFolderUrl.appendingPathComponent("DBCDTest.sqlite")
    let backupUrl2 = backUpFolderUrl.appendingPathComponent("DBCDTest.sqlite-wal")
    let backupUrl3 = backUpFolderUrl.appendingPathComponent("DBCDTest.sqlite-shm")
    let sourceSqliteURLs = [backupUrl1, backupUrl2, backupUrl3]

    let container = NSPersistentContainer(name: "DBCDTest")

    do {
        // Replace current persistent store with the restore/backup persistent store.
        try container.persistentStoreCoordinator.replacePersistentStore(at: currentPSUrl1, destinationOptions: nil, withPersistentStoreFrom: backupUrl1, sourceOptions: nil, ofType: NSSQLiteStoreType)
        // Delete the restore/backup files from the Application directory.
        do {
            for index in 0..<sourceSqliteURLs.count {
                try FileManager.default.removeItem(at: sourceSqliteURLs[index])
            }
        } catch let error {
            print("Failed to delete sqlite files.")
            print(error.localizedDescription)
        }
    } catch let error {
        print("Failed to replace persistent store.")
        print(error.localizedDescription)
    }
}

Instead of creating a new NSPersistentContainer I have tried using dependency injection to reference the one created in the AppDelegate and using that to perform the swap but it fails trying to replace the persistent stores. Could it be some kind of NSManagedObjectContext issue? Could it need flushing prior to accessing the new data store?

I've managed to pull an answer together from a number of answers to similar questions, in particular this one from Tom Harrington . In short what I needed to do was:

  1. Create a new NSPersistentContainer .
  2. Replace the current persistent store with the restore/backup persistent store using the replacePersistentStore method on the persistentStoreCoordinator .
  3. Destroy the backup store.
  4. Delete the files associated with the backup store.
  5. Rebuild the Core Data stack in the AppDelegate .
  6. Save, nil and then reinitialise the MasterViewController's managedObjectContext and NSFetchedResultsController .

Number 6 took some time for me to see the light. My final restore method is:

func restorePSFromBackup() {
    // Current persistent store is in the ApplicationSupport directory.
    let currentPSFolderUrl = FileManager.default.urls(for: .applicationSupportDirectory, in:.userDomainMask).first!
    // The current Core Data file (url).
    let currentPSUrl1 = currentPSFolderUrl.appendingPathComponent("DBCDTest.sqlite")
    // Backup persistent store with which to restore is in the Documents directory.
    let backUpFolderUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
    // The 3 backup Core Data files (urls).
    let backupUrl1 = backUpFolderUrl.appendingPathComponent("DBCDTest.sqlite")
    let backupUrl2 = backUpFolderUrl.appendingPathComponent("DBCDTest.sqlite-wal")
    let backupUrl3 = backUpFolderUrl.appendingPathComponent("DBCDTest.sqlite-shm")
    let sourceSqliteURLs = [backupUrl1, backupUrl2, backupUrl3]

    let container = NSPersistentContainer(name: "DBCDTest")

    do {
        // Replace current persistent store with the restore/backup persistent store.
        try container.persistentStoreCoordinator.replacePersistentStore(at: currentPSUrl1, destinationOptions: nil, withPersistentStoreFrom: backupUrl1, sourceOptions: nil, ofType: NSSQLiteStoreType)
        // Destroy the backup store.
        try container.persistentStoreCoordinator.destroyPersistentStore(at: backupUrl1, ofType: NSSQLiteStoreType, options: nil)
        // Delete the restore/backup files from the Application directory.
        do {
            for index in 0..<sourceSqliteURLs.count {
                try FileManager.default.removeItem(at: sourceSqliteURLs[index])
            }
        } catch let error {
            print("Failed to delete sqlite files.")
            print(error.localizedDescription)
        }
        // Rebuild the AppDelegate's Core Data stack.
        (UIApplication.shared.delegate as! AppDelegate).persistentContainer = NSPersistentContainer(name: "DBCDTest")
        (UIApplication.shared.delegate as! AppDelegate).persistentContainer.loadPersistentStores(completionHandler: { (storeDescription, error) in
            print(NSPersistentContainer.defaultDirectoryURL())
            if let error = error as NSError? {
                fatalError("Unresolved error \(error), \(error.userInfo)")
            } else {
                // Save, nil and then reinitialise the MasterViewController managedObjectContext and NSFetchedResultsController.
                do {
                    try self.masterViewController.managedObjectContext?.save()
                } catch let error {
                    print("Failed to save managedObjectContext.")
                    print(error.localizedDescription)
                }
                self.masterViewController.managedObjectContext = nil
                self.masterViewController.managedObjectContext = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
                self.masterViewController._fetchedResultsController = nil
                let _ = self.masterViewController.fetchedResultsController
            }
        })
    } catch let error {
        print("Failed to replace persistent store.")
        print(error.localizedDescription)
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM