Search code examples
iosswiftcore-datacloudkit

CoreData+CloudKit | On/off iCloud sync toggle


I want to give the user the option to toggle iCloud sync on and off.

After researching for a while, I saw that one way one could achieve this is by setting the cloudKitContainerOptions.

So I would set it to nil if I don't want my database to be synched.

if(!UserDefaultsManager.shared.iCloudSyncOn) {
    description.cloudKitContainerOptions = nil
}

That's all working fine, but I haven't found a way to do that during runtime.

I have tried to reinitialize my container when the user toggles, so that my container has different cloudKitContainerOptions depending on the choice.

But this would only return me an error, when saving the context, saying: Thread 1: "Illegal attempt to establish a relationship 'addEntries' between objects in different contexts ..., which I believe is due to the reinitialization.

I think I would have to pass down the newly created context to my whole view hierarchy, anything that caches the moc?

Here would be a simplified snipped of my CoreDataStack:

func setupContainer() -> NSPersistentContainer {
    let container = NSPersistentCloudKitContainer(name: "...")
    
    guard let description = container.persistentStoreDescriptions.first else { ... }

    ...
    
    if(!UserDefaultsManager.shared.iCloudSyncOn) {
        description.cloudKitContainerOptions = nil
    }
    
    container.loadPersistentStores(completionHandler: { ... })
    
    ...

    return container
}

When the user toggles, setupContainer() gets called.

Any help would be awesome, alternative ways are of course also welcomed!

Thanks.


Solution

  • I have been able to make it work!

    My problem specifically was that I haven't updated my already fetched objects (with the old context) after reinitializing the persistenceContainer (which created a new context).

    So, directly after calling setupContainer(), a simple fetch (with the new context) for all my objects was enough.

    self.container = setupContainer()
    CoreDataManager.shared.fetchAllItem()
    

    Additionals

    I have encountered one more problem due to the reinitialization, which was a warning that multiple NSEntityDescriptions were claiming NSManagedObject Subclass of my entities.

    This answer fixed it for me.

    Final Code

    Maybe this could help you out. Works fine for me. (iOS 14.2) Slightly modified.

    PS: Instead of setting the cloudKitContainerOptions I ended up switching between NSPersistentCloudKitContainer and NSPersistenttContainer.

    lazy var container: NSPersistentContainer = {
        setupContainer()
    }()
    
    
    func updateContainer() {
        saveContext()
        container = setupContainer()
        CoreDataManager.shared.fetchAllItems()
    }
    
    
    private func setupContainer() -> NSPersistentContainer {
        let iCloud = UserDefaultsManager.shared.settingICloudSynch
        
        do {
            let newContainer = try PersistentContainer.getContainer(iCloud: iCloud)
            guard let description = newContainer.persistentStoreDescriptions.first else { fatalError("No description found") }
            
            if iCloud {
                newContainer.viewContext.automaticallyMergesChangesFromParent = true
                newContainer.viewContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy
            } else {
                description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
            }
    
            description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
    
            newContainer.loadPersistentStores { (storeDescription, error) in
                if let error = error as NSError? { fatalError("Unresolved error \(error), \(error.userInfo)") }
            }
            
            return newContainer
            
        } catch {
            print(error)
        }
        
        fatalError("Could not setup Container")
    }
    
    final class PersistentContainer {
        
        private static var _model: NSManagedObjectModel?
        
        private static func model(name: String) throws -> NSManagedObjectModel {
            if _model == nil {
                _model = try loadModel(name: name, bundle: Bundle.main)
            }
            return _model!
        }
        
        
        private static func loadModel(name: String, bundle: Bundle) throws -> NSManagedObjectModel {
            guard let modelURL = bundle.url(forResource: name, withExtension: "momd") else {
                throw CoreDataModelError.modelURLNotFound(forResourceName: name)
            }
    
            guard let model = NSManagedObjectModel(contentsOf: modelURL) else {
                throw CoreDataModelError.modelLoadingFailed(forURL: modelURL)
           }
            return model
        }
    
        
        enum CoreDataModelError: Error {
            case modelURLNotFound(forResourceName: String)
            case modelLoadingFailed(forURL: URL)
        }
    
        
        public static func getContainer(iCloud: Bool) throws -> NSPersistentContainer {
            let name = "YOUR APP"
            if iCloud {
                return NSPersistentCloudKitContainer(name: name, managedObjectModel: try model(name: name))
            } else {
                return NSPersistentContainer(name: name, managedObjectModel: try model(name: name))
            }
        }
    }