I have two contexts, MAIN and VIEW. I construct an object in MAIN and it appears in VIEW (which I use to display it in the UI). Then I delete the object in MAIN. Because the UI holds a reference to the object in VIEW, VIEW records it as a pending delete (Problem 1). I don't understand why it does this nor can I find this behaviour documented. Docs for deletedObjects say "objects that will be removed from their persistent store during the next save". This has already happened!
(Problem 2) Then I rollback the VIEW context, and the object is resurrected. awakeFromInsert is called again. While the object (correctly) does not appear in a freshly executed fetch request, it does appear in the @FetchRequest of the SwiftUI View which is now displaying stale data. I cannot figure out how to get SwiftUI to execute the fetch request again (I know I can force regeneration of the UI, but would like to avoid this).
This is self-contained demonstration of the problem that can be run in a Playground. Press Create, then Delete (note console output), then Rollback (note console output, and that element count changes from 0 to 1 in the UI)
import CoreData
import SwiftUI
@objc(TestEntity)
class TestEntity : NSManagedObject, Identifiable{
@NSManaged var id : UUID?
override func awakeFromInsert() {
print("Awake from insert")
if id == nil {
// Avoid resetting ID when we resurrect the phantom delete
self.id = UUID()
}
super.awakeFromInsert()
}
class func add(in context: NSManagedObjectContext) -> UUID {
let id = UUID()
context.performAndWait {
let mo = TestEntity(context: context)
mo.id = id
}
return id
}
class func fetch(in context: NSManagedObjectContext) -> [TestEntity] {
let fr = TestEntity.fetchRequest()
return try! context.fetch(fr) as! [TestEntity]
}
}
class CoreDataStack {
// Main is attached to the store
var main : NSManagedObjectContext!
// View is a child context of main and used to display the UI
var view : NSManagedObjectContext!
// Set up a simple entity with an ID attribute
func getEntities() -> [NSEntityDescription] {
let testEntity = NSEntityDescription()
testEntity.managedObjectClassName = "TestEntity"
testEntity.name = "TestEntity"
let idAttribute = NSAttributeDescription()
idAttribute.name = "id"
idAttribute.type = .uuid
testEntity.properties.append(idAttribute)
return [testEntity]
}
init() {
let model = NSManagedObjectModel()
model.entities = getEntities()
let container = NSPersistentContainer(name: "TestModel", managedObjectModel: model)
let description = NSPersistentStoreDescription()
description.type = NSInMemoryStoreType
container.persistentStoreDescriptions = [description]
container.loadPersistentStores { desc, error in
if error != nil {
fatalError("Failed to set up coredata")
}
}
main = container.viewContext
view = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
view.automaticallyMergesChangesFromParent = true
view.parent = main
}
func create() {
let entityId = TestEntity.add(in: main)
main.performAndWait {
try! main.save()
}
}
func delete() {
main.performAndWait {
if let mo = TestEntity.fetch(in: main).first {
main.delete(mo)
try! main.save()
}
}
self.view.perform {
// We only find that we have a pending delete here if we hold a reference to the object, e.g. in the UI via @FetchRequest
if(self.view.deletedObjects.count != 0) {
print("!!! view has a pending delete, even though main has saved the delete !!!")
}
}
}
func rollback() {
self.view.perform {
self.view.rollback()
// PROBLEM We now have a resurrected object. Note that awakeFromInsert
// was called again.
}
}
}
import SwiftUI
import PlaygroundSupport
let stack = CoreDataStack()
struct ContentView: View {
@FetchRequest(sortDescriptors: []) private var entities: FetchedResults<TestEntity>
@State var renderID = UUID()
var body: some View {
VStack {
Text("\(entities.count) elements")
Button("Create") {
stack.create()
}
Button("Delete") {
stack.delete()
}
Button("Rollback") {
stack.rollback()
// PROBLEM After rollback we get the element displaying in
// the UI again, even though it isn't present in a freshly
// executed fetch request.
// The @FetchRequest is picking up the resurrected TestEntity in view
// But not actually issuing a fetch.
self.renderID = UUID()
entities.nsPredicate
}
}.id(renderID)
}
}
//stack.execute()
let view = ContentView()
.environment(\.managedObjectContext, stack.view)
PlaygroundPage.current.setLiveView(view)