Swiftui Holding Reference to Deleted Core Data Object Causing Crash

SwiftUI holding reference to deleted core data object causing crash

I basically had the same issue. It seems that SwiftUI loads every view immediately, so the view has been loaded with the Properties of the existing CoreData Object. If you delete it within the View where some data is accessed via @ObservedObject, it will crash.

My Workaround:

  1. The Delete Action - postponed, but ended via Notification Center
    Button(action: {
//Send Message that the Item should be deleted
NotificationCenter.default.post(name: .didSelectDeleteDItem, object: nil)

//Navigate to a view where the CoreDate Object isn't made available via a property wrapper
self.presentationMode.wrappedValue.dismiss()
})
{Text("Delete Item")}

You need to define a Notification.name, like:

extension Notification.Name {

static var didSelectDeleteItem: Notification.Name {
return Notification.Name("Delete Item")
}
}

  1. On the appropriate View, lookout for the Delete Message

// Receive Message that the Disease should be deleted
.onReceive(NotificationCenter.default.publisher(for: .didSelectDeleteDisease)) {_ in

//1: Dismiss the View (IF It also contains Data from the Item!!)
self.presentationMode.wrappedValue.dismiss()

//2: Start deleting Disease - AFTER view has been dismissed
DispatchQueue.main.asyncAfter(deadline: .now() + TimeInterval(1)) {self.dataStorage.deleteDisease(id: self.diseaseDetail.id)}
}


  1. Be safe on your Views where some CoreData elements are accessed - Check for isFault!

VStack{
//Important: Only display text if the disease item is available!!!!
if !diseaseDetail.isFault {
Text (self.diseaseDetail.text)
} else { EmptyView() }
}

A little bit hacky, but this works for me.

Crash when delete a record from coredata in SwiftUI

I had the same problem as you.

Probably it is a bad idea to use your own id property to make it Identifiable because Core Data is setting all the properties to nil when the object is deleted, even if you declared it differently in your Swift CoreData class.
When deleting your entity, the id property gets invalidated and the objects .isFault property is set to true, but the SwiftUI ForEach still holds some reference to this ID object (=your UUID) to be able to calculate the "before" and "after" state of the list and somehow tries to access it, leading to the crash.

Therefore the following recommendations:

  1. Protect the detail view (in the ForEach loop by checking isFault:
if entity.isFault {
EmptyView()
}
else {
// your regular view body
}

  1. Expect your id property to be nil, either by defining it accordingly in your core data model as optional
@NSManaged public var id: UUID?

or by not relying on the Identifiable protocol in the SwiftUI ForEach loop:

ForEach(entities, id: \.self) { entity in ... }

or

ForEach(entities, id: \.objectID) { entity in ... }

Conclusion: you really do not need to make all your CoreData properties Swift Optionals. It's simply important that your id property referenced in the ForEach loop handles the deletion (=setting its value to nil) gracefully.

Swift Combine: sink() called after core data entity deletion

You can use faultingState to track this scenario. From documentation:

0 if the object is fully initialized as a managed object and not transitioning to or from another state, otherwise some other value. This property allows you to determine if an object is in a transitional phase when receiving a key-value observing change notification.

So you can ignore this event like this:

.sink { [weak self] sold in
guard let self = self, car.faultingState == 0 else { return }
//
}

If you wanna actually cancel this sink, you can store cancellables inside the object so you can cancel them during prepareForDeletion.

To do this you need to change object code generation, more info can be found here. Change to Category/Extension - in this case you can create a class and override prepareForDeletion, and Xcode will still generate all the properties for you.

Now you can move all publisher logic into your class:

@objc(Car)
public class Car: NSManagedObject {
private var subscribers = Set<AnyCancellable>()

func observe<Value: Equatable>(keyPath: KeyPath<Item, Value>, receiveValue: @escaping ((Value) -> Void)) {
publisher(for: keyPath, options: [.new])
.removeDuplicates()
.receive(on: RunLoop.main)
.sink(receiveValue: receiveValue)
.store(in: &subscribers)
}

public override func prepareForDeletion() {
super.prepareForDeletion()
subscribers.removeAll()
}
}

Or just use it to store cancellables.

Why saving managed object context changes isDeleted value?

Why try self.moc.save() changes self.item.isDeleted from true to
false? It happens after I delete a Core Data object (isDeleted changes
to true), but later saving managed object context changes it to false.
Why is that?

It behaves as documented - returns true before save, and not in other cases

Here is snapshot of Apple documentation for NSManagedObject:

Summary

A Boolean value that indicates whether the managed object will be
deleted during the next save. Declaration

var isDeleted: Bool { get } Discussion

true if Core Data will ask the persistent store to delete the object
during the next save operation, otherwise false. It may return false
at other times, particularly after the object has been deleted. The
immediacy with which it will stop returning true depends on where the
object is in the process of being deleted. If the receiver is a fault,
accessing this property does not cause it to fire.



Related Topics



Leave a reply



Submit