Search code examples
iosswiftcore-dataicloudnsmanagedobjectcontext

managedObjectContext for NSManagedObject becomes nil after restoring app from inactive


Maybe you can help, I couldn't find anything similar in other questions, so I guess I might miss something obvious.

I have the CoreData + iCloud App in Swift.

Problem Scenario:

  1. Launch app
  2. NSManagedObject read/update
  3. Press "Home" (make app inactive)
  4. Restore app
  5. NSManagedObject read/update

If I have logged into iCloud on my device then this works fine.

If I am logged out of iCloud, then all app works fine, just if I do the problem scenario, then on step 5 the managedObjectContext for NSManagedObject is nil, so I cannot make any changes to it, and because of lost context it off course crashes as soon as I need the context for an existing object.

My Question:

  1. Why this is happening as in Problem Scenario?
  2. How to fix this, so that if app has become inactive, and then active, without iCloud user logged in, the CoreData keeps working?

My CoreDataStack:

class CoreDataStack: CustomStringConvertible
{
    static let sharedManager = CoreDataStack()
    static let applicationDocumentsDirectoryName = "iCloud.com.myCompany.myAppID"
    static let errorDomain = "CoreDataStack"

    static let modelName = "DB"
    static let storeName = "DB"
    static var storeFileName: String
    {
        return storeName + ".sqlite"
    }
    var options : [String : AnyObject]?

    var inMemory: Bool = false


    var description: String
    {
        var desc = "context: \(self.managedObjectContext)\n" +
            "modelName: \(CoreDataStack.modelName)" +
            "storeURL: \(self.storeURL)"

        desc += "\nPersistent Stores:\n"
        for store in persistentStoreCoordinator.persistentStores
        {
            desc += "* \(store.URL!.absoluteString)"
        }

        return desc
    }


    lazy var managedObjectModel: NSManagedObjectModel =
    {
        let modelURL = NSBundle.mainBundle().URLForResource(modelName, withExtension: "momd")!
        return NSManagedObjectModel(contentsOfURL: modelURL)!
    }()


    lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator =
    {
        let coordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)

        do
        {
            if self.inMemory
            {
                try coordinator.addPersistentStoreWithType(
                    NSInMemoryStoreType,
                    configuration: nil,
                    URL: nil,
                    options: nil)
            } else
            {
                try coordinator.addPersistentStoreWithType(
                    NSSQLiteStoreType,
                    configuration: nil,
                    URL: self.storeURL,
                    options: self.options)
            }
        } catch var error as NSError
        {
            VTLog.error("Persistent Store Error: \(error)")
        } catch
        {
            fatalError("Error creating Persistent Store!")
        }
        return coordinator
    }()


    /// The directory the application uses to store the Core Data store file.
    lazy var applicationSupportDirectory: NSURL =
    {
        let fileManager = NSFileManager.defaultManager()
        let urls = fileManager.URLsForDirectory(.ApplicationSupportDirectory, inDomains: .UserDomainMask)
        let applicationSupportDirectoryURL = urls.last!
        let applicationSupportDirectory =
            applicationSupportDirectoryURL.URLByAppendingPathComponent(applicationDocumentsDirectoryName)

        do
        {
            let properties = try applicationSupportDirectory.resourceValuesForKeys([NSURLIsDirectoryKey])

            if let isDirectory = properties[NSURLIsDirectoryKey] as? Bool where isDirectory == false
            {
                let description = NSLocalizedString("Could not access the application data folder.",
                                                    comment: "Failed to initialize applicationSupportDirectory.")
                let reason = NSLocalizedString("Found a file in its place.",
                                               comment: "Failed to initialize applicationSupportDirectory.")

                throw NSError(domain: errorDomain, code: 201, userInfo:
                [
                    NSLocalizedDescriptionKey: description,
                    NSLocalizedFailureReasonErrorKey: reason
                ])
            }
        } catch let error as NSError where error.code != NSFileReadNoSuchFileError
        {
            fatalError("Error occured: \(error).")
        } catch
        {
            let path = applicationSupportDirectory.path!

            do
            {
                try fileManager.createDirectoryAtPath(path, withIntermediateDirectories:true, attributes:nil)
            }
            catch
            {
                fatalError("Could not create application documents directory at \(path).")
            }
        }

        return applicationSupportDirectory
    }()


    /// URL for the main Core Data store file.
    lazy var storeURL: NSURL =
    {
        return self.applicationSupportDirectory.URLByAppendingPathComponent(storeFileName)
    }()


    lazy var managedObjectContext: NSManagedObjectContext =
    {
        let context = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType)
        context.persistentStoreCoordinator = self.persistentStoreCoordinator
        return context
    }()


    // ****************************************
    // MARK: - iCloud Sync
    // ****************************************

    var updateContextWithUbiquitousContentUpdates: Bool = false
    {
        willSet
        {
            ubiquitousChangesObserver = newValue ? NSNotificationCenter.defaultCenter() : nil
        }
    }


    private var ubiquitousChangesObserver: NSNotificationCenter?
    {
        didSet
        {
            oldValue?.removeObserver(
                self,
                name: NSPersistentStoreDidImportUbiquitousContentChangesNotification,
                object: persistentStoreCoordinator)

            ubiquitousChangesObserver?.addObserver(
                self,
                selector: #selector(self.persistentStoreDidImportUbiquitousContentChanges(_:)),
                name: NSPersistentStoreDidImportUbiquitousContentChangesNotification,
                object: persistentStoreCoordinator)


            oldValue?.removeObserver(
                self,
                name: NSPersistentStoreCoordinatorStoresWillChangeNotification,
                object: persistentStoreCoordinator)

            ubiquitousChangesObserver?.addObserver(
                self,
                selector: #selector(self.persistentStoreCoordinatorWillChangeStores(_:)),
                name: NSPersistentStoreCoordinatorStoresWillChangeNotification,
                object: persistentStoreCoordinator)
        }
    }


    @objc func persistentStoreDidImportUbiquitousContentChanges(notification: NSNotification)
    {
        VTLog.debug("Merging ubiquitous content changes")
        VTLog.debug(notification)

        self.managedObjectContext.performBlock
        {
            self.managedObjectContext.mergeChangesFromContextDidSaveNotification(notification)
        }
    }


    @objc func persistentStoreCoordinatorWillChangeStores(notification: NSNotification)
    {
        VTLog.debug(notification)

        if managedObjectContext.hasChanges
        {
            do
            {
                try managedObjectContext.save()
            } catch let error as NSError
            {
                print("Error saving: \(error)", terminator: "")
            }
        }
        managedObjectContext.reset()
    }


    // ***********************************************
    // * Data: iCloud Container Actions
    // ***********************************************

    func deleteiCloudContainer()
    {
        VTLog.debug("Deleting iCloud Container...")

        let currentStore = managedObjectContext.persistentStoreCoordinator!.persistentStores.last!

        VTLog.debug("Located data store [\(currentStore)]")

        managedObjectContext.reset()
        VTLog.debug("managedObjectContext.reset() - OK")

        do
        {
            try managedObjectContext.persistentStoreCoordinator?.removePersistentStore(currentStore)
            VTLog.debug("removePersistentStore() - OK")
        } catch let error as NSError
        {
            VTLog.error("Could not remove persistent store [\(currentStore)]: \(error)")
        }

        do
        {
            try NSPersistentStoreCoordinator.removeUbiquitousContentAndPersistentStoreAtURL(
                currentStore.URL!, options: currentStore.options)
            VTLog.debug("removeUbiquitousContentAndPersistentStoreAtURL() - OK")
        } catch let error as NSError
        {
            VTLog.error("Could not remove Ubiquitous Content and Persistent Store at URL [\(currentStore)]: \(error)")
        }
    }


    //*******************************************
    // MARK: - Init
    //*******************************************

    init(inMemory:Bool = false)
    {
        self.inMemory = inMemory

        self.options = [NSMigratePersistentStoresAutomaticallyOption: true,
            NSInferMappingModelAutomaticallyOption: true,
            NSPersistentStoreUbiquitousContentNameKey: CoreDataStack.storeName]
    }

}

Additional info that might help:

  1. All this is on Simulator, regardless of version: iOS 9.2, iOS 9.3.
  2. When I have logged into iCloud, then all works fine.
  3. I have noticed that DB.sqlite file actually doesn't exist on path storeURL, it's created on path as seen here below, but it's the same with iCloud login or without, so I don't know if it should be that way.
  4. When I restore app, I see the following sequence of actions:

@ 2016-04-12 11:30:36: AppDelegate: applicationDidEnterBackground:133: (thread): {number = 10, name = main}

@ 2016-04-12 11:30:37: AppDelegate: applicationWillEnterForeground:141: (thread): {number = 11, name = main}

2016-04-12 11:30:37.150 Count Myself[57886:19968276] -PFUbiquitySwitchboardEntryMetadata setUseLocalStorage:: CoreData: Ubiquity: nobody~sim7CC36E42-82CB-5152-91BE-4DD26FE0A420:DB Using local storage: 1 for new NSFileManager current token (null)

@ 2016-04-12 11:30:37: CoreDataStack: persistentStoreCoordinatorWillChangeStores:203: NSConcreteNotification 0x7fd3e8d8fdc0 {name = NSPersistentStoreCoordinatorStoresWillChangeNotification; object = ; userInfo = { NSPersistentStoreUbiquitousTransitionTypeKey = 2; added = ( " (URL: file:///Users/maris/Library/Developer/CoreSimulator/Devices/F9A852DA-595C-4DE2-ADD7-7DECD7D814AD/data/Containers/Data/Application/107B6DB1-C4DC-4626-8933-DACD0575F184/Library/Application%20Support/iCloud.com.myCompany.myAppID/CoreDataUbiquitySupport/nobody~sim7CC36E42-82CB-5152-91BE-4DD26FE0A420/DB/local/store/DB.sqlite)" ); removed = ( " (URL: file:///Users/maris/Library/Developer/CoreSimulator/Devices/F9A852DA-595C-4DE2-ADD7-7DECD7D814AD/data/Containers/Data/Application/107B6DB1-C4DC-4626-8933-DACD0575F184/Library/Application%20Support/iCloud.com.myCompany.myAppID/CoreDataUbiquitySupport/nobody~sim7CC36E42-82CB-5152-91BE-4DD26FE0A420/DB/local/store/DB.sqlite)" ); }} (thread): {number = 12, name = main}

@ 2016-04-12 11:30:37: AppDelegate: applicationDidBecomeActive:152: context: modelName: DBstoreURL: file:///Users/maris/Library/Developer/CoreSimulator/Devices/F9A852DA-595C-4DE2-ADD7-7DECD7D814AD/data/Containers/Data/Application/107B6DB1-C4DC-4626-8933-DACD0575F184/Library/Application%20Support/iCloud.com.myCompany.myAppID/DB.sqlite Persistent Stores: * file:///Users/maris/Library/Developer/CoreSimulator/Devices/F9A852DA-595C-4DE2-ADD7-7DECD7D814AD/data/Containers/Data/Application/436959B5-7850-4156-AB3D-A11BE72FF1AF/Library/Application%20Support/iCloud.com.myCompany.myAppID/CoreDataUbiquitySupport/nobody~sim7CC36E42-82CB-5152-91BE-4DD26FE0A420/DB/local/store/DB.sqlite

  1. The problem appears when I set this: stack.updateContextWithUbiquitousContentUpdates = true But if I don't set this to true, I think I will not get updates from iCloud populated instantly.
  2. See on GitHub the Xcode project that demonstrates the problem: https://github.com/marisveide/iCloudCoreDataProblem (read AppDelegate.swift file comments on top) That would be fantastic to see the code changes in separate branch the solution not to lose the Context.

Solution

  • Ok, I have found the solution - maybe this helps somebody else too.

    Basically, by checking if iCloud is enabled, and then tailoring the CoreDataStack setup.

    Like this:

    init(inMemory:Bool = false)
    {
        self.inMemory = inMemory
    
        self.options = [NSMigratePersistentStoresAutomaticallyOption: true,
                        NSInferMappingModelAutomaticallyOption: true]
    
        if iCloudEnabled
        {
            self.options?[NSPersistentStoreUbiquitousContentNameKey] = CoreDataStack.storeName
            self.monitorUbiquitousContentUpdatesIfiCloudEnabled()
        }
    
    }
    

    More details can be found in GitHub project sample - diff of the commit which solves me the problem: https://github.com/marisveide/iCloudCoreDataProblem/commit/1d2da6c000bee7a66274192da2a637ba7c8cabf5#diff-8efc4cba62b5f1efad252e6f27b5b30b

    Whole project for download here: https://github.com/marisveide/iCloudCoreDataProblem

    Whew... that was "fun" for last 3 days of debugging and searching.