I have the following CoreData stack in my app:
let mom = NSManagedObjectModel(contentsOf:modelURL)!
managedObjectContext = NSManagedObjectContext(concurrencyType:.mainQueueConcurrencyType)
privateContext = NSManagedObjectContext(concurrencyType:.privateQueueConcurrencyType)
coordinator = NSPersistentStoreCoordinator(managedObjectModel:mom)
managedObjectContext.automaticallyMergesChangesFromParent = true
privateContext.automaticallyMergesChangesFromParent = true
privateContext.persistentStoreCoordinator = coordinator
managedObjectContext.parent = privateContext
managedObjectContext.mergePolicy = NSMergePolicy(merge: .mergeByPropertyObjectTrumpMergePolicyType)
privateContext.mergePolicy = NSMergePolicy(merge: .mergeByPropertyObjectTrumpMergePolicyType)
So, managedObjectContext is what I use for Main Actor's reads/writes, while the privateContext is responsible for heavy lifting – writing/reading from SQL store itself.
When I need to save data, I use the following method:
func save() {
if !(managedObjectContext.hasChanges) {
return
}
do {
try self.managedObjectContext.safeSave()
do {
try self.privateContext.safeSave()
}
catch let error {
// HANDLE ERROR
}
}
catch let error as NSError {
// HANDLE ERROR
}
}
// .....
extension NSManagedObjectContext {
func safeSave() throws {
try performAndWait {
if hasChanges {
try save()
}
}
}
}
The problem that I encounter is that whenever I save an entity, any SwiftUI view which observes it gets updated twice. And the reason for a second update is that due to automaticallyMergesChangesFromParent set to true, the second update is when an entity turns into a fault.
This is extremely confusing and I do not understand why this occurs. For example:
struct CellView: View {
@ObservedObject private var topic: TopicStatistic
var body: some View {
print("CELL BODY \(topic)")
return Button {
topic.isSelected = !topic.isSelected
DataStack.shared.save() //calls save method illustrated above
} label: { Text("Test") }
}
}
This prints two "CELL BODY" whenever I tap on the button, the first one is:
CELL BODY <Topic: 0x600002137b60> (entity: Topic; id: 0x85da4595cd988912 <x-coredata://F05EF0BE-58F4-423C-A12E-E4FF67BE17D5/Topic/p65>; data: {
isSelected = 0;
// other properties here
})
The second one is:
CELL BODY <Topic: 0x60000213dc20> (entity: Topic; id: 0x8db98711ee4d8ece <x-coredata://F05EF0BE-58F4-423C-A12E-E4FF67BE17D5/Topic/p53>; data: <fault>)
As you can see, the second update is when Topic turns into a fault.
What I have verified:
- Removing any one of
automaticallyMergesChangesFromParentremoves this issue - If I do not use the "middle"
privateContextand attachmanagedObjectContextdirectly to the persistent store, the issue also resolves.
However, none if these are good options for me. I have another background context which is used for Cloud sync, so I do want to both keep this setup and to ensure that changes are propagated.
Does anyone know why objects turn into faults when I would assume that changes go one way to the persistent store when I click on a button?