When using @FetchRequest to populate a List, SwiftUI will attempt to access a deleted NSManagedObject even after it has been deleted from the store (for example, via swipe action).
This bug is very common and easy to reproduce: simply create a new Xcode project with the default SwiftUI + CoreData template. Then replace ContentView with this code:
struct ContentView: View {
@Environment(\.managedObjectContext) private var moc
@FetchRequest(sortDescriptors: []) private var items: FetchedResults<Item>
var body: some View {
List {
ForEach(items) { item in
ItemRow(item: item)
}
.onDelete {
$0.map{items[$0]}.forEach(moc.delete)
try! moc.save()
}
}
.toolbar {
Button("Add") {
let newItem = Item(context: moc)
newItem.timestamp = Date()
try! moc.save()
}
}
}
}
struct ItemRow: View {
@ObservedObject var item: Item
var body: some View {
Text("\(item.timestamp!)")
}
}
Add a few items to the list, then swipe to delete a row: the app will crash. An ItemRow
is attempting to draw with the now-deleted Item
.
A common workaround is to wrap the whole subview in a fault check:
struct ItemRow: View {
@ObservedObject var item: Item
var body: some View {
if !item.isFault {
Text("\(item.timestamp!)")
}
}
}
But this is a poor solution and has side effects (objects can be faults when not deleted).
This answer suggests that wrapping the deletion in viewContext.perform{}
would work, but the crash still occurs for me.
Any better solutions/workarounds out there?