Issue
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.
Answered By – Reinhard Männer
Answer Checked By – Gilberto Lyons (BugsFixing Admin)