After Core Data Migration Main TableView Crashes Swift

时间:2015-07-31 19:49:27

标签: ios core-data

I have a fairly standard Core Data fed tableView with cell data populated from a fetchedResultsController.

Everything works as expected until I do a Core Data migration. The purpose of the lightweight migration is to provide a simple backup not to change the model. The store uses SQLite. The plan is to do the migration to generate the new data files and then to remove the new store and install the original store in order to keep the original file names.

The view for the backup procedure is also a tableView. Once the migration is completed, the new file is visible in the backup tableView. Upon clicking the "back" button to return to the original tableView, the data is visible as expected, but clicking on any row in the tableView causes an immediate crash and I'm presented with the dreaded "Object's persistent store is not reachable from this NSManagedObjectContext's coordinator" error.

I've been struggling with this for a week. I must be missing a basic concept. Any help would be appreciated. (iOS 8, Xcode 6.4)

Here are the fetchedResultsController variables. Again these work all the time until a migration is made:

var myFetchedResultsController: NSFetchedResultsController? = nil

var fetchedResultsController: NSFetchedResultsController {

    managedObjectContext = kAppDelegate.managedObjectContext

    if myFetchedResultsController != nil {
        return myFetchedResultsController!
    }//if my ! nil

    let fetchRequest = NSFetchRequest()

    let entity = NSEntityDescription.entityForName("Patient", inManagedObjectContext: managedObjectContext)
    fetchRequest.entity = entity
    fetchRequest.fetchBatchSize = 50

    //Sort keys
    let sortDescriptor = NSSortDescriptor(key: "dateEntered", ascending: false)
    let sortDescriptors = [sortDescriptor]

    fetchRequest.sortDescriptors = [sortDescriptor]

    let aFetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: managedObjectContext, sectionNameKeyPath: nil, cacheName: nil)

    var countError : NSError? = nil
    var count = managedObjectContext.countForFetchRequest(fetchRequest, error: &countError)

    println("The count is \(count)")
    //after creating a backup, this count is ALWAYS zero - never the real count

    aFetchedResultsController.delegate = self
    myFetchedResultsController = aFetchedResultsController

    var error: NSError? = nil
    if !myFetchedResultsController!.performFetch(&error) {
         // Don't forget the code to handle the error appropriately.
         println("Unresolved error \(error), \(error!.userInfo)")
        //Remove this
         abort()
    }//if !my

    return myFetchedResultsController!
}//var fetchedResultsController

The two functions for the backup procedure:

func createLocalBackupFile() {

    let dateFormatter = NSDateFormatter()
    dateFormatter.dateFormat = "yyyyMMddHHmmss"
    let theDateTime = NSDate()
    let formattedDateTime = dateFormatter.stringFromDate(theDateTime)
    let backupFileName : String = "BiopBak" + formattedDateTime + ".sqlite"
    println("backupFileName is \(backupFileName)")

    let psu : CRSPersistentStoreUtilities = CRSPersistentStoreUtilities()//the function below is in this class

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), { () -> Void in
        //println("In a background queue, creating the backup file")
        psu.backupTheStore(backupFileName)

        //go back to the main queue
        dispatch_async(dispatch_get_main_queue(), { () -> Void in
            println("Back on main queue after creating the backup file")

            if (self.backupSqlFiles.count == 1 && self.backupSqlFiles[0] == "Placeholder for empty list") {
                self.backupSqlFiles.append(backupFileName.stringByDeletingPathExtension)
                self.backupSqlFiles.removeAtIndex(0)

            } else {
                self.backupSqlFiles.append(backupFileName.stringByDeletingPathExtension)

            }//if placeholder is only record in database - else

            self.tableView.reloadData()
            println("backupSqlFiles[] = \(self.backupSqlFiles)")

        })//back to main block - inner
    })//background processing block - outer

}//createLocalBackupFile

func backupTheStore(newSQLFileName : String) -> NSPersistentStore? {

        let storeType = NSSQLiteStoreType
        var migrateError : NSError?
        var currentStore : NSPersistentStore = kAppDelegate.persistentStoreCoordinator?.persistentStores.last! as! NSPersistentStore
        let options = [NSMigratePersistentStoresAutomaticallyOption: true,
            NSInferMappingModelAutomaticallyOption: true]

        let fileManager = NSFileManager.defaultManager()
        let paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)
        let docsDir = paths[0] as! String
        let docsDirURL = NSURL(fileURLWithPath: docsDir)

        let originalStoreURL : NSURL = docsDirURL?.URLByAppendingPathComponent("BiopLogCloud.sqlite") as NSURL!
        var newStoreURL : NSURL = docsDirURL?.URLByAppendingPathComponent(newSQLFileName) as NSURL!

    kAppDelegate.persistentStoreCoordinator?.migratePersistentStore(currentStore, toURL: newStoreURL, options: options, withType: storeType, error: &migrateError)

    currentStore = kAppDelegate.persistentStoreCoordinator?.persistentStores.last! as! NSPersistentStore

    var removeStoreError : NSError?
    var theStores = kAppDelegate.persistentStoreCoordinator?.persistentStores

    if let theStores2 = theStores {
        for removeStore in theStores2 {
            var removed : Bool = true
            kAppDelegate.persistentStoreCoordinator?.removePersistentStore(removeStore as! NSPersistentStore, error: &removeStoreError)

            if (removeStoreError != nil) {
                println("Unable to remove persistent store \(removeStore)")
            }
        }//for in
    }//if let theStores

    var addStoreError : NSError?

    kAppDelegate.persistentStoreCoordinator?.addPersistentStoreWithType(storeType,
        configuration: nil,
        URL: originalStoreURL,
        options: options,
        error:&addStoreError)

    if (addStoreError != nil) {
        println("Unable to add persistent store \(originalStoreURL)")
        //change this to add a user alert
    }//if

    //this does not seem to do any good
    let ptvc : PatientTableViewController = PatientTableViewController()

    dispatch_async(dispatch_get_main_queue()) {
        () -> Void in
        ptvc.tableView.reloadData()
    }//block

    return thisStore

}//backupTheStore

2 个答案:

答案 0 :(得分:2)

What appears to be happening is:

  1. You're displaying some managed objects in your table view, which were fetched before the migratePersistentStore call.
  2. You do the migratePersistentStore call. Your backupTheStore method will implicitly remove the original persistent store (as part of the migrate call) but it tries to fix things up by removing the new persistent store and re-adding the old one.
  3. You then try to use one of the managed objects from step 1.

The problem, I think, is that although you have re-added the persistent store used to fetch those managed objects, your migration process has lost the connection from the managed objects to the store. The migrate call clears out the persistent store coordinator state, which breaks the managed object / persistent store connection, and adding the persistent store doesn't re-create that connection. (Maybe it should but that's apparently not how it's designed).

As a result, you have managed objects that the persistent store coordinator can't relate to a persistent store file, and you crash when you try to use them.

Reloading the table view isn't enough because it will just reload the same managed objects from the fetched results controller. You should also make sure to call performFetch on the fetched results controller to make it re-fetch its data. If that's not enough, set myFetchedResultsController to nil and then reload the table, so that you'll get a completely new fetch.

答案 1 :(得分:0)

新的重要数据 - 这不仅仅是备份和还原问题。这个应用程序是一个iCloud应用程序,所以当然原始商店应该在本地ubiquity容器而不是app文档目录。我上面的代码确实适用于非iCloud设置。我已将originalStoreURL调整为指向本地ubiquity容器,现在我可以恢复原始数据存储。

正如Tom在上面指出的那样,通过引用应用程序文档目录来恢复商店,我实际上创建了一个新的商店,它在每个备份过程后都会保留 - 但这是一个本地的非iCloud商店。 / p>