CoreData+CloudKit | On/off iCloud sync toggle
Asked Answered
S

1

11

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.

Southsouthwest answered 18/12, 2020 at 10:50 Comment(0)
S
10

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))
        }
    }
}
Southsouthwest answered 18/12, 2020 at 15:49 Comment(6)
Nice work! i also want to do this only based on in app purchase. How would you implement this with the pre-made persistenceController inside persistence.swift created by Xcode. or how can i replace that one with this?Erikerika
Yes sure, you can modify the persistenceController to your needs. But if you are only looking to make in app purchases work, you don't need a CloudKitContainer.Southsouthwest
I'm using your solution with iOS15 Swift-UI app. When I switch off the CloudKit synchronization and update the container, the sync is still active. After restarting the app manually the sync is deactivated. Was there any change in iOS15?Ending
I'm having the same issue. When you disable iCloud sync, changes still sync except the app no longer gets updates from changes made outside of it. Once the app is relaunched, then it doesn't sync anymore. Does anybody have a solution?Envy
Are you sure these changes are changes made after the deactivations?Incubate
Do we need to fetch all items on loading the container? Can I do it if I were to reload the screen?Commons

© 2022 - 2024 — McMap. All rights reserved.