CoreData & CloudKit toggle iCloud sync (enable/disable)

604 views Asked by At

Since my last question (here toggle between local and iCloud CoreData store) I was able to make a lot of progress.

I am switching between NSPersistentCloudKitContainer and NSPersistenttContainer

But...

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.

This is the same problem some people are describing in the comments here...
CoreData+CloudKit | On/off iCloud sync toggle

But I wasn't able to find a solution.

MyApp.swift

@main
struct MyApp: App {
    @StateObject private var persistenceContainer = PersistenceController.shared

    var body: some Scene {        
        WindowGroup {
            ContentView()
                .environmentObject(CoreBluetoothViewModel())
                .environment(\.managedObjectContext, persistenceContainer.container.viewContext)
        }
    }
}

PersistenceController

import CoreData

class PersistenceController: ObservableObject{
    
    static let shared = PersistenceController()
    
    lazy var container: NSPersistentContainer = {
        setupContainer()
    }()
    
    init() {
        container = setupContainer()
    }
    
    func updateContainer() {
        saveContext()
        container = setupContainer()
        saveContext()
    }
    
    private func setupContainer() -> NSPersistentContainer {
        let iCloud = UserDefaults.standard.bool(forKey: "iCloud")
        
        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.cloudKitContainerOptions = nil
            }
            
            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")
    }
    
    private func saveContext() {
        do {
            try container.viewContext.save()
        } catch {
            let error = error as NSError
            fatalError("ERROR: \(error)")
        }
    }
}

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 = "LogModel"
        if iCloud {
            return NSPersistentCloudKitContainer(name: name, managedObjectModel: try model(name: name))
        } else {
            return NSPersistentContainer(name: name, managedObjectModel: try model(name: name))
        }
    }
}

Does anyone have any idea how I could solve this problem?

1

There are 1 answers

3
Oliver Rider On

fairly new to SwiftUi still but have been struggling with finding a solution to this problem as well. By the looks of it I’ve been following all of the stackoverflow questions and answers as yourself.

For context I get a very inconsistent result between a iPad and a iPhone with a version of my code which is pretty close to mine, where sometimes it works and I can toggle on and off cloudkit.

However...

After playing around with the code and commenting on and off lines and reading some more, this line of code stood out as a question for me.

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

surely the answer is in its name. RemoteChangeNotificationPost.

what I have done its put that line in with the

if iCloud {
    newContainer.viewContext.automaticallyMergesChangesFromParent = true
    newContainer.viewContext.mergePolicy =   NSMergeByPropertyStoreTrumpMergePolicy

    description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
}

so it only "Posts Changes" when iCloud is enabled.

now when I toggle off cloudkit. no updates are sent through.

When you toggle it back on, it does require a change to happen on the device to trigger a sync it seams but it I feel its a step closer to a result?

hit me down everyone if im doing anything wrong wouldn't want to be pushing a bad practice but so far.... it works till I find a better solution.


message from Apple:

If you need to store some data on device and provide an option for users to opt in the CloudKit synchronization, consider creating a local store and a CloudKit store to manage the data separately. When users choose to opt in, you move the data from local store to the CloudKit one, and let `NSPersistentCloudKitContainer` take care the rest. By using a local  store, you control what data and when to be moved to the CloudKit store.

For details about managing multiple stores with one Core Data stack, see Linking Data Between Two Core Data Stores:
<https://developer.apple.com/documentation/coredata/linking_data_between_two_core_data_stores>