swiftswiftuicore-dataicloudcloudkit

CoreData stored locally and with iCloud disappears when iCloud sync is turned off


I want to have my CoreData to be synced to iCloud which works perfectly right now. But when I turn off iCloud sync in the simulator all of the data is "lost". But when I turn on iCloud sync again the data comes right back which is nice. But I want the data to be stored locally as well, if the user doesn't want to fill up his iCloud storage.

The scenario being: the user sees a lot of data (which will never be GB of data but maybe some users are picky even with MB of data) fill up his iCloud and want the sync turned of for my app. When the sync is turned of all of the user's data disappears locally, but is still in the cloud. I want the data not to disappear when the sync is turned of.

Is my "request" possible?

This is my code:

import CoreData

class DataController: ObservableObject {

  lazy var persistentContainer: NSPersistentCloudKitContainer = {
    let container = NSPersistentCloudKitContainer(name: "CoreData")
    let address = Bundle.main.path(forResource: "CoreData", ofType: ".momd")
    
    // Create a store description for a local store
    let localStoreLocation = URL(fileURLWithPath: "\(address!)/Local.sqlite")
    let localStoreDescription =
        NSPersistentStoreDescription(url: localStoreLocation)
    localStoreDescription.configuration = "Local"
    
    // Create a store description for a CloudKit-backed local store
    let cloudStoreLocation = URL(fileURLWithPath: "\(address!)/Cloud.sqlite")
    let cloudStoreDescription =
        NSPersistentStoreDescription(url: cloudStoreLocation)
    cloudStoreDescription.configuration = "Cloud"

    // Set the container options on the cloud store
    cloudStoreDescription.cloudKitContainerOptions =
        NSPersistentCloudKitContainerOptions(
            containerIdentifier: "iCloud.com.my.identifier")
    
    // Update the container's list of store descriptions
    container.persistentStoreDescriptions = [
        cloudStoreDescription,
        localStoreDescription
    ]
    
    // Load both stores
    container.loadPersistentStores { storeDescription, error in
        guard error == nil else {
            fatalError("Could not load persistent stores. \(error!)")
        }
    }
    
    return container
}()

init() {
    persistentContainer.loadPersistentStores { description, error in
        if let error = error {
            print("Core Data failed to load: \(error.localizedDescription)")
            }
        }
    persistentContainer.viewContext.automaticallyMergesChangesFromParent = true
    }
}

Solution

  • You have two persistent store descriptions, but that doesn't mean that when you save an object that it goes into both persistent stores. Your code would need to do that.

    A managed object belongs to only one persistent store. If you have more than one store (as in your code), what happens? Since you have multiple persistent stores where an object could belong to either one, it goes into the first one in the list-- which is why they're all cloud objects here.

    If you want to save the same data in both stores, you would need to do something like this:

    1. Every time you create a new managed object, make a duplicate with the same data.
    2. Use NSManagedObjectContext's function assign(_ object: Any, to store: NSPersistentStore) to tell it to put the duplicate into the right store.
    3. When you fetch data, use NSFetchRequest's property affectedStores to make sure you only fetch from one of the two stores. Otherwise you'll have duplicate results, since you would have results from both stores.

    Since the duplicate is a different object instance, it will have a different NSManagedObjectID. You would probably need to add your own unique ID field so you can match an object from one store to the corresponding one from the other store.

    You'll need to do similar things any time you change a managed object. If you change one, find the duplicate from the other store and make the same change.

    If this sounds complicated, it is. This is not a simple problem to attack. It's not impossible but it isn't going to be easy to get it right.

    [If you were using different model configurations you could have different managed object types go into specific stores. Each object would still go into a single store, not both, but the choice would be automatic. This doesn't help solve your problem but the docs for the assign method mention this situation so I thought I'd mention it too.]