Search code examples
swiftcore-dataswiftuicloudkit

CloudKit + Core Data: How to write or read the public database only


I have a record type that is in both the Public database configuration schema and the private database configuration schema.

when I write a record type using the PersistentStore.shared.context it writes the record to both the private database and the public database. When I query the record type using @FetchRequest, it returns the records from both the public and private database.

How do I write or read to just the public or just the private database?

My PersistentStore Stack is basically a copy paste from apples WWDC code:

class PersistentStore: ObservableObject {
    var context: NSManagedObjectContext { persistentContainer.viewContext }
    
    // One line singleton
    static let shared = PersistentStore()
    
    private let persistentStoreName: String = "XXXX"
    let containerIdentifier: String = "iCloud.com.XXXX.XXXX"
    
    // MARK: - Core Data stack
    lazy var persistentContainer: NSPersistentContainer = {
        //let container = NSPersistentContainer(name: persistentStoreName)
        // OR - Include the following line for use with CloudKit - NSPersistentCloudKitContainer
        let container = NSPersistentCloudKitContainer(name: persistentStoreName)
        
        // Enable history tracking
        // (to facilitate previous NSPersistentCloudKitContainer's to load as NSPersistentContainer's)
        // (not required when only using NSPersistentCloudKitContainer)
        guard let persistentStoreDescriptions = container.persistentStoreDescriptions.first else {
            fatalError("\(#function): Failed to retrieve a persistent store description.")
        }
        let storesURL = persistentStoreDescriptions.url!.deletingLastPathComponent()
        
        //private database
        let privateStoreURL = storesURL.appendingPathComponent("\(persistentStoreName)-private.sqlite")
        let privateStoreDescription = NSPersistentStoreDescription(url: privateStoreURL)
        privateStoreDescription.configuration = "Private"
        privateStoreDescription.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: containerIdentifier)
        privateStoreDescription.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
        privateStoreDescription.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
        privateStoreDescription.cloudKitContainerOptions?.databaseScope = .private
        
        //public database
        let publicStoreURL = storesURL.appendingPathComponent("\(persistentStoreName)-public.sqlite")
        let publicStoreDescription = NSPersistentStoreDescription(url: publicStoreURL)
        publicStoreDescription.configuration = "Public"
        publicStoreDescription.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: containerIdentifier)
        publicStoreDescription.cloudKitContainerOptions?.databaseScope = .public
        
        container.persistentStoreDescriptions = [publicStoreDescription, privateStoreDescription]
        
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error {
                // Replace this implementation with code to handle the error appropriately.
                fatalError("Unresolved error \(error)")
            }
        })
        
        // Include the following line for use with CloudKit - NSPersistentCloudKitContainer
        container.viewContext.automaticallyMergesChangesFromParent = true
        // Include the following line for use with CloudKit and to set your merge policy, for example...
        
        container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
        
        return container
    }()
    
    // Mark the class private so that it is only accessible through the singleton `shared` static property
    private init() {}
    
    // MARK: - Core Data Saving and "other future" support (such as undo)
    
    func save() {
        let context = persistentContainer.viewContext
        if context.hasChanges {
            do {
                try context.save()
            } catch {
                // Customize this code block to include application-specific recovery steps.
                let nserror = error as NSError
                fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
            }
        }
    }
}

Solution

  • I had a similar requirement: My app uses CloudKit & Core Data with the private and the shared database, and the entities (Item or Place) are assigned to both stores, but only one of them must be used at a time.
    To handle this, I use a var currentlyUsedStores that is set either to the private persistent store or the shared persistent store.
    New entities are initialized either in the viewContext or a backgroundContext, but before the context is saved, the context sends a notification:

    // Register for notifications that the view context will save.
    NotificationCenter.default.addObserver(self, 
                                           selector: #selector(viewContextOrBackgroundContextWillSave), 
                                           name: .NSManagedObjectContextWillSave,
                                           object: viewContext)
    
    // Register for notifications that the background context will save.
    NotificationCenter.default.addObserver(self, 
                                           selector: #selector(viewContextOrBackgroundContextWillSave), 
                                           name: .NSManagedObjectContextWillSave,
                                           object: backgroundContext)
    
     @objc func viewContextOrBackgroundContextWillSave(_ notification: Notification) {
        guard let context = notification.object as? NSManagedObjectContext  else { return }
        let inserts = context.insertedObjects
        let itemsAndPlaces = inserts.filter({ $0 is Item || $0 is Place })
        itemsAndPlaces.forEach({ context.assign($0, to: currentlyUsedStores.first!) })
    }    
    

    Since assign can be used before a newly inserted object is saved, all new objects are now stored only in the currently used persistent store.

    To fetch only from the currently used store, the affectedStores property has to be set in the fetch request, e.g.

    let itemFetchRequest = NSFetchRequest<Item>(entityName: Item.entityName)
    itemFetchRequest.affectedStores = currentlyUsedStores  
    

    If one then switches the currently used store (that can also be saved, e.g. in the user defaults, one has only to update var currentlyUsedStores.

    EDIT:
    A NSBatchInsertRequest is also executed by a NSManagedContext. Thus, as long as objects are newly inserted in the context, they can be assigned to the relevant persistent store in the same way.