swiftcore-datacloudkit

How to save to specific Stores/Configurations in Core Data


I'm currently syncing core data with the CloudKit private and public databases, as you can see in the code below, I'm saving the private database in the default configuration in Core Data and the public in a configuration called Public everything works fine when NSPersistentCloudKitContainer syncs, what I'm having an issue with is trying to save to the public data store PublicStore, for instance when I try to save with func createIconImage(imageName: String) it saves the image to the "default" store, not the PublicStore(Public configuration).

What could I do to make the createIconImage() function save to the PublicStore sqlite database?

    class CoreDataManager: ObservableObject{
        static let instance = CoreDataManager()
        private let queue = DispatchQueue(label: "CoreDataManagerQueue")
        
        @AppStorage(UserDefaults.Keys.iCloudSyncKey) private var iCloudSync = false
        
        lazy var context: NSManagedObjectContext = {
        return container.viewContext
        }()
        
        lazy var container: NSPersistentContainer = {
        return setupContainer()
        }()
        
        init(inMemory: Bool = false){
            if inMemory {
                container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
            }
        }
        
        func updateCloudKitContainer() {
            queue.sync {
                container = setupContainer()
            }
        }

        private func getDocumentsDirectory() -> URL {
            return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
        }

        private func getStoreURL(for storeName: String) -> URL {
            return getDocumentsDirectory().appendingPathComponent("\(storeName).sqlite")
        }
        
        func setupContainer()->NSPersistentContainer{
            let container = NSPersistentCloudKitContainer(name: "CoreDataContainer")
            let cloudKitContainerIdentifier = "iCloud.com.example.MyAppName"
            
            guard let description = container.persistentStoreDescriptions.first else{
                fatalError("###\(#function): Failed to retrieve a persistent store description.")
            }
            
            description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
            description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
            
            if iCloudSync{
                if description.cloudKitContainerOptions == nil {
                    let options = NSPersistentCloudKitContainerOptions(containerIdentifier: cloudKitContainerIdentifier)
                    description.cloudKitContainerOptions = options
                }
            }else{
                print("Turning iCloud Sync OFF... ")
                description.cloudKitContainerOptions = nil
            }
            
            // Setup public database
            let publicDescription = NSPersistentStoreDescription(url: getStoreURL(for: "PublicStore"))
            publicDescription.configuration = "Public" // this is the configuration name
            if publicDescription.cloudKitContainerOptions == nil {
                let publicOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: cloudKitContainerIdentifier)
                publicOptions.databaseScope = .public
                publicDescription.cloudKitContainerOptions = publicOptions
            }
            container.persistentStoreDescriptions.append(publicDescription)
            
            container.loadPersistentStores { (description, error) in
                if let error = error{
                    print("Error loading Core Data. \(error)")
                }
            }
            container.viewContext.automaticallyMergesChangesFromParent = true
            container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy

            return container
        }
        
        func save(){
            do{
                try context.save()
                //print("Saved successfully!")
            }catch let error{
                print("Error saving Core Data. \(error.localizedDescription)")
            }
        }
    }


    class PublicViewModel: ObservableObject {
        let manager: CoreDataManager
        
        @Published var publicIcons: [PublicServiceIconImage] = []
        
        init(coreDataManager: CoreDataManager = .instance) {
            self.manager = coreDataManager
        }

        func createIconImage(imageName: String) {
            let newImage = PublicServiceIconImage(context: manager.context)

            newImage.imageName = imageName
            newImage.id = UUID()
            
            save()
        }
        
        func save() {
            self.manager.save()
        }
    }

Solution

  • After days of trying different things, I accidentally found out that the order of descriptions in the container array plays a crucial role in how Core Data prioritizes loading configurations and saving entities. In my original code, I was loading the default configuration first as shown below...

     guard let description = container.persistentStoreDescriptions.first else{
          fatalError("###\(#function): Failed to retrieve a persistent store description.")
      }
    

    Then I was appending the public configuration as follows...

    container.persistentStoreDescriptions.append(publicDescription)
    

    Leaving the public configuration at the end of the persistentStoreDescriptions array, and apparently, the way Core Data works is that it searches the first configuration, and if the entity you're saving exists in the first configuration, it saves it in that configuration otherwise it keeps looping through all configs until it finds the entity in one of the configurations but since (by default) the Default configuration contains all entities, it was always saving to the default configuration so, the solution is to always leave the default configuration as the last item in the array.

    Solution:

    Here is what I did that solved my issue:

    Replaced this line...

    container.persistentStoreDescriptions.append(publicDescription)
    

    with...

    container.persistentStoreDescriptions = [publicDescription, description]
    

    Again, I basically added the Public configuration to the first configuration in the array. The key here is to always leave the default configuration at last, regardless of how many configurations you have.

    FYI - The sample project from the Linking Data Between two Core Data Stores led me to try to reorder the configurations in the array.

    https://developer.apple.com/documentation/coredata/linking_data_between_two_core_data_stores

    EDIT 06/09/25:
    A little update on configurations: When you create a Core Data Model, the Default configuration is automatically generated. All new entities are automatically added to this configuration by default and cannot be removed from it. If you're not using other custom configurations, you'll automatically be working with the Default configuration, where all records are stored—no issues there since you're only using one configuration.

    In my case, the challenge arose when I initially used the Default configuration but later introduced CloudKit and to separate records for the Public CloudKit database, I created a new configuration and called it Public. However, since entities added to the Public configuration also exist in the Default configuration, and since I was loading the Default configuration first (by placing it as the first item in the container.persistentStoreDescriptions array), everything was saved there instead.

    Core Data loads configurations in the order they were added to the container.persistentStoreDescriptions array and searches for entities by name. If a matching entity is found in the first loaded store, it saves the record in that configuration (in my case it was the Default configuration). The key here is to always load the Default configuration last—either by placing it as the last item in the persistentStoreDescription array or not loading it at all if it isn’t needed. My recommendation is that you always use a custom configuration even if you're only using a single store and I would name it Private. This way you will be already prepared in case you later want to add CloudKit features such as, saving to the Public database or Sharing records between users or simply to have your data organized by store. Again, try to avoid using the Default configuration if you can, to avoid issues later on when adding other custom configurations. Please note that if you start saving everything in the Default configuration and you later on want to stop using it, you will need to do a migration to migrate the data from the Default to the custom configuration.