Post

Replies

Boosts

Views

Activity

Object deleted in MOC not properly deleted in child MOC
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)
4
0
329
Oct ’24