8 Replies
      Latest reply on Nov 13, 2019 1:19 AM by acwrightdesign
      tschmitz Level 1 Level 1 (0 points)

        Is there a way to pause or disable syncing via NSPersistentCloudKitContainer at runtime? I want to give my users the option to enable or disable syncing, but there doesn't seem to be a good way to do it. I tried reloading the persistent stores and setting the store description's cloudKitContainerOptions to nil, but if I do that after first initialization, I get an error:

         

        error: Store opened without NSPersistentHistoryTrackingKey but previously had been opened with NSPersistentHistoryTrackingKey - Forcing into Read Only mode

         

        Anyone know of another way to do this?

         

        I submitted a request via Feedback Assistant for a flag to enable or disable syncing (FB6156182). I know the user could theoretically go into the Settings app and enable or disable iCloud access for the app as a whole, but that's not very discoverable, and disables all iCloud access. I'm looking for something a little more fine-grained (limited to the NSPersistentCloudKitContainer) and that I could control in my app's UI.

        • Re: Toggle sync with NSPersistentCloudKitContainer
          .Nick Apple Staff Apple Staff (40 points)

          That error is because you also removed the history tracking option. Which you shouldn't do after you've enabled it.

           

          You can disable CloudKit sync simply by setting the cloudKitContainer options property on your store description to nil.

           

          However, you should leave history tracking on so that NSPersitentCloudKitContainer can catch up if you turn it on again.

            • Re: Toggle sync with NSPersistentCloudKitContainer
              Ash Level 1 Level 1 (0 points)

              How should we go about leaving history tracking enabled once we set the cloudKitContainer options to nil?

              • Re: Toggle sync with NSPersistentCloudKitContainer
                asnow003 Level 1 Level 1 (0 points)

                How do you set the store description to nil.....I see its key/value but I tried to set it on the description after loading the store and it says its read only?

                 

                   // MARK: - Core Data stack

                  

                    lazy var persistentContainer: NSPersistentCloudKitContainer = {

                        /*

                         The persistent container for the application. This implementation

                         creates and returns a container, having loaded the store for the

                         application to it. This property is optional since there are legitimate

                         error conditions that could cause the creation of the store to fail.

                         */

                        let container = NSPersistentCloudKitContainer(name: "name")

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

                          {tried to set it here storeDescription.options = nil}

                  • Re: Toggle sync with NSPersistentCloudKitContainer
                    sdmarcotte Level 1 Level 1 (0 points)

                    What worked for me was setting cloudKitContainerOptions = nil before calling container.loadPersistentStores

                     

                        lazy var persistentContainer: NSPersistentContainer = {

                              .

                              .

                              .

                              if !UserDefaultsManager.shared.syncWithCloudKit {

                                   container.persistentStoreDescriptions.forEach {  $0.cloudKitContainerOptions = nil  }

                              }

                     

                              container.loadPersistentStores( completionHandler: { (_, error) in ... } )

                     

                    This is a hack since this works only when persistentContainer is first initialized.

                     

                    Trying to do this after persistentContainer is initialized would require that I safely teardown and reinitialize the CoreDataStack.  Reinitializing CoreDataStack creates a new managedObjectContext which then must be passed down the view hierarchy and to any services or managers that cache the managedObjectContext.

                     

                    There are many reasons to want to toggle or pause syncing with CloudKit, and since NSPersistentCloudKitContainer is privy to all the inner workings of syncing with CloudKit, NSPersistentCloudKitContainer should be the one to handle this.

                     

                     

                    Some methods I could find useful would be:

                     

                         enum SyncMode {

                              case disabled

                              case paused       // Paus temporarily, useful for batch processing

                              case enabled

                         }

                     

                         persistentContainer.setSyncMode( _ mode: SyncMode )

                     

                         persistentContainer.syncAfter( someFutureTime )     // Sync after some time in the future.

                     

                     

                    Steve

                • Re: Toggle sync with NSPersistentCloudKitContainer
                  ddijitall Level 1 Level 1 (0 points)

                  Did you get this to work? I have a similar need, that is, to offer iCloud storage and sync as a premium feature. I have CloudKit sync working but how do I switch it off? I want to disable CloudKit sync and only enable it when a customer pays for the premium version. I've tried setting the container's cloudKitContainerOptions property to nil but CloudKit sync still happens.

                   

                  Here's my Core Data stack:

                   

                  private lazy var persistentContainer: NSPersistentContainer = {

                        

                          let container = NSPersistentCloudKitContainer(name: modelName)

                        

                          let localDescription = NSPersistentStoreDescription(url: configurationsURL.appendingPathComponent("local.sqlite", isDirectory: false))

                          localDescription.configuration = "Local"

                        

                          let cloudDescription = NSPersistentStoreDescription(url: configurationsURL.appendingPathComponent("cloud.sqlite", isDirectory: false))

                          cloudDescription.configuration = "Cloud"

                          cloudDescription.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: cloudKitContainerIdentifier)

                          cloudDescription.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)

                          cloudDescription.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)

                        

                          container.persistentStoreDescriptions = [localDescription, cloudDescription]

                   

                          container.loadPersistentStores { (storeDescription, error) in

                              if let error = error as NSError? {

                                  fatalError("###\(#function): Failed to load persistent stores:\(error)")

                              }

                          }

                        

                          container.viewContext.name = "main"

                          container.viewContext.transactionAuthor = appTransactionAuthorName

                          container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy

                          container.viewContext.automaticallyMergesChangesFromParent = true

                        

                          do {

                              try container.viewContext.setQueryGenerationFrom(.current)

                          } catch {

                              fatalError("###\(#function): Failed to pin viewContext to the current generation:\(error)")

                          }

                        

                          NotificationCenter.default.addObserver(self, selector: #selector(type(of: self).storeRemoteChange(_:)), name: .NSPersistentStoreRemoteChange, object: nil)

                        

                  //        do {

                  //            //try container.initializeCloudKitSchema(options: [.dryRun, .printSchema])

                  //            try container.initializeCloudKitSchema()

                  //        } catch {

                  //            let nserror = error as NSError

                  //            print("Could not initialize CloudKit schema: \(error.localizedDescription)")

                  //            print(nserror.code)

                  //            print(nserror.domain)

                  //        }

                        

                          return container

                      }()

                   

                  Ands here's a function where I'm trying to toggle the CloudKit sync:

                   

                  func enableiCloud(_ isEnabled: Bool) {

                          let cloudDescription = persistentContainer.persistentStoreDescriptions.first() {

                              $0.configuration == "Cloud"

                          }

                          guard cloudDescription != nil else { return }

                          if isEnabled {

                              cloudDescription!.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: cloudKitContainerIdentifier)

                              cloudDescription!.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)

                          } else {

                              cloudDescription!.cloudKitContainerOptions = nil

                              cloudDescription!.setOption(false as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)

                          }

                      }

                  • Re: Toggle sync with NSPersistentCloudKitContainer
                    nitramluap Level 1 Level 1 (0 points)

                    I spoke to an Apple Engineer about this and they suggested the best way was to make your container return either an NSPersistentContainer or NSPersistentCloudKitContainer depending on whether you users have selected iCloud syncing internally in your app - you can use a UserDefaults bool to store this.

                     

                    This works because NSPersistentCloudKitContainer is a subclass of NSPersistentContainer.

                     

                    You will also need to set the NSPersistentHistoryTrackingKey to true on the vanillla container so changes are recorded should they switch iCloud back on. There doesn't appear to be any need to set any options manually on NSPersistentCloudKitContainer as they're enabled by default.

                     

                    I have a PersistenceService class which manages my MOC and in it, this is how I set up the container:

                     

                        static var iCloudSyncData = UserDefaults.standard.bool(forKey: "iCloudSyncData")
                        
                        static var persistentContainer:  NSPersistentContainer  = {
                            let container: NSPersistentContainer?
                            if iCloudSyncData {
                                container = NSPersistentCloudKitContainer(name: "MYAPP")
                            } else {
                                container = NSPersistentContainer(name: "MYAPP")
                                let description = container!.persistentStoreDescriptions.first
                                // This allows a 'non-iCloud' sycning container to keep track of changes if a user changes their mind
                                // and turns it on.
                                description?.setOption(true as NSNumber,
                                                       forKey: NSPersistentHistoryTrackingKey)
                            }
                            container!.loadPersistentStores(completionHandler: { (storeDescription, error) in
                                if let error = error as NSError? {
                                    // Handle the errors
                                    fatalError("Unresolved error \(error), \(error.userInfo)")
                                }
                            })
                            // As NSPersistentCloudKitContainer is a subclass of NSPersistentContainer, you can return either.
                            return container!
                        }()

                     

                    This all works beautifully on my devices, and syncing (and toggling on/off) all works... but despite 'Deploying the Schema to Production' not one of my users in the 'real world' is able to sync their data with CloudKit. It seems like there is something wrong with the way their devices initially subscribe and I can't find an easy way to troubleshoot this as the API is somewhat opaque. I'm in discussions with an Apple Engineer about this so will hopefully have a solution soon.