7

I just added the option for a user to toggle Cloud sync inside my app where I save whether or not the user wants to use iCloud sync in UserDefaults under "useCloudSync". I load my persistentContainer when the app runs with:

class CoreDataManager {
    static let sharedManager = CoreDataManager()
    private init() {}

    lazy var persistentContainer: NSPersistentContainer = {
        var useCloudSync = UserDefaults.standard.bool(forKey: "useCloudSync")

        let containerToUse: NSPersistentContainer?
        if useCloudSync {
           containerToUse = NSPersistentCloudKitContainer(name: "App")
        } else {
            containerToUse = NSPersistentContainer(name: "App")
            let description = containerToUse!.persistentStoreDescriptions.first
            description?.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
      }

        guard let container = containerToUse, let description = container.persistentStoreDescriptions.first else {
            fatalError("Hey Listen! ###\(#function): Failed to retrieve a persistent store description.")
        }
        description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)

        container.loadPersistentStores(completionHandler: { (storeDescription, error) in

      ...

      return container
   }
}

When a user toggles cloud sync with a UISwitcher, I change the value for "useCloudSyc" in UserDefaults, but the app doesn't use the NSPersistentCloudKitContainer until they force close the app and re-run it again. I would like the container to change right when the user toggles the switch to start loading their data from iCloud.

How can I change to and from a NSPersistentCloudKitContainer when the user toggles "CloudSync"?

ap123
  • 916
  • 1
  • 8
  • 22

1 Answers1

7

Here is possible approach

extension UserDefaults { // helper key path for observing
    @objc dynamic var useCloudSync: Bool {
        return bool(forKey: "useCloudSync")
    }
}

class CoreDataManager {
    static let sharedManager = CoreDataManager()

    private var observer: NSKeyValueObservation?
    private init() {
    }

    lazy var persistentContainer: NSPersistentContainer = {
        setupContainer()
    }()

    private func setupContainer() -> NSPersistentContainer {

        if nil == observer {
            // setup observe for defults changed
            observer = UserDefaults.standard.observe(\.useCloudSync) { [weak self] (_, _) in
                try? self?.persistentContainer.viewContext.save() // save anything pending
                if let newContainer = self?.setupContainer() {
                    self?.persistentContainer = newContainer
                }
            }
        }

        let useCloudSync = UserDefaults.standard.bool(forKey: "useCloudSync")

        let containerToUse: NSPersistentContainer?
        if useCloudSync {
            containerToUse = NSPersistentCloudKitContainer(name: "App")
        } else {
            containerToUse = NSPersistentContainer(name: "App")
            let description = containerToUse!.persistentStoreDescriptions.first
            description?.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
        }

        guard let container = containerToUse, let description = container.persistentStoreDescriptions.first else {
            fatalError("Hey Listen! ###\(#function): Failed to retrieve a persistent store description.")
        }
        description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)

        container.loadPersistentStores { (storeDescription, error) in
            //      ...
        }

        return container
    }
}
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • This worked! The only thing I noticed that's a bit off is whenever I set the "useCloudSync", `setupContainer()` is called on twice. Is there a possible fix for this? I checked to see if I was setting "useCloudSync" twice and I'm not – ap123 Apr 19 '20 at 09:10
  • @ap123, I can't say definite, `cause I don't have all your code, but you can set breakpoint into first line of `setupContainer`, so you'll can catch the origin of first & second call to it, thus will be clear where probably some additional condition needed. – Asperi Apr 19 '20 at 09:27
  • 2
    Hello, I just caught a bug I didn't see before. When switching the persistent containers I get this error for every item in core data database: "CoreData: warning: Multiple NSEntityDescriptions claim the NSManagedObject subclass 'App.Item' so +entity is unable to disambiguate." where app is the name of my app and item is one of the entity types. I believe this is happening because the old container is still open after the switch. Is there a way to close the old persistent container before using the new one maybe somewhere above `self?.persistentContainer = newContainer` from above?? – ap123 May 18 '20 at 05:56
  • Have you fixed it? – Kai Zheng Dec 18 '20 at 12:52
  • This fixed it for me: https://stackoverflow.com/a/51857486/8700044 – Kai Zheng Dec 18 '20 at 15:06