-1

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?

Hundley
  • 3,167
  • 3
  • 23
  • 45
  • I can't replicate this. I'm on Xcode 14.0 beta, but I've used similar techniques in Xcode 13 without issue. – ScottM Jun 10 '22 at 21:35
  • @ScottM that's very interesting - I'm testing on Xcode 14 right now. Have been seeing the same issue since SwiftUI 1.0 on Xcode 11. – Hundley Jun 10 '22 at 21:41
  • @ScottM just to be sure: are you testing on iOS? – Hundley Jun 10 '22 at 21:44
  • Yes I am. I'm actually working on a brand new app right now – I've been keeping the `Item` list view around while I add my own objects and views as a baseline, so if it works when my own code doesn't I've got a comparison. And this deletion code works for me! – ScottM Jun 10 '22 at 21:53
  • And you're passing Item as @ObservedObject just like my code? That's the important distinction... I'd love to find out what's different about our environments. – Hundley Jun 10 '22 at 21:55
  • Ah found the discrepancy – i'd tweaked my code to get rid of the force unwrap on `timestamp!` which is where your code crashes (unless you check `isFault` as you have done). Switching to a `swipeAction` to initiate the delete operation at the row level, rather than as a bulk operation, works though. – ScottM Jun 10 '22 at 22:08

1 Answers1

1

This does seem to be the result of the ObservedObject switching to a fault as part of the delete operation, while timestamp! is force unwrapped. The force unwrap works for the full object but not the fault.

An approach which does seem to work is to remove the .onDelete action at the ForEach level, and replace it with a swipeAction at the row level:

ForEach(items) { item in
  ItemRow(item: item)
    .swipeActions {
      Button("Delete", role: .destructive) {
        viewContext.delete(item)
        try? viewContext.save()
      }
    }
}

In any respect, it shows that even with a simple NSManagedObject like Item, relying on force unwrapping of attributes comes with risks.

ScottM
  • 7,108
  • 1
  • 25
  • 42
  • That's definitely a better workaround than the others I've seen. Still - it's unfortunate that this hasn't been resolved by now. Even the engineers at WWDC recommend implicitly unwrapping NSManagedObject properties to work with Swift. – Hundley Jun 10 '22 at 22:19