SwiftUI List crash with @FetchRequest and @ObservedObject

Hi, I am running into a reproducible SwiftUI List crash when using @FetchRequest based on an @ObservedObject. The crash happens only when deleting the last item in a section. All other deletes and inserts (that I've tested so far) seem to work fine. I'm hoping I can figure out why this happens, and if there is a workaround that I can use.

The crash looks something like this:

*** Assertion failure in -[SwiftUI.UpdateCoalescingCollectionView _endItemAnimationsWithInvalidationContext:tentativelyForReordering:animator:collectionViewAnimator:], UICollectionView.m:10643
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'attempt to delete item 17 from section 0 which only contains 17 items before the update'

The setup: I have a Core Data one-to-many relationship ... in this case, a Contact that has many Notes saved to it. When you select a Contact from a list, it goes to a ContactDetailsView which has some details of the contact, and a list of 'notes' saved to it.

struct ContactDetailsView: View {
    @Environment(\.managedObjectContext) private var viewContext
    @ObservedObject var contact: Contact // -> making this 'let' works
    
    var body: some View {
        VStack (alignment: .leading) {
            
            Text(contact.firstName ?? "")
            Text(contact.lastName ?? "")

            NotesListView(contact: contact)
            
            Button("Add Test Notes") {
                let note1 = Notes(context: viewContext)
                note1.noteMonth = "Feb"
                note1.noteDetails = "Test1"
                note1.noteDate = Date()
                note1.contact = contact
                
                try? viewContext.save()
            }
        }
        .padding()
    }
}

The NotesListView has a @SectionedFetchRequest (the error is the same if I use a regular @FetchRequest).

struct NotesListView: View {
    @Environment(\.managedObjectContext) private var viewContext
    @ObservedObject var contact: Contact // -> making this 'let' works
    
    @SectionedFetchRequest var sectionNotes: SectionedFetchResults<String, Notes>
    @State private var selectedNote: Notes?
    
    init(contact: Contact) {
        self.contact = contact
        
        let fetchRequest = Notes.fetchRequest()
        fetchRequest.predicate = NSPredicate(format: "contact == %@", contact)
        fetchRequest.sortDescriptors = [NSSortDescriptor(key: "noteDate", ascending: true)]
        
        _sectionNotes = SectionedFetchRequest(fetchRequest: fetchRequest, sectionIdentifier: \.noteMonth!)
    }
    var body: some View {
        List (selection: $selectedNote){
            ForEach(sectionNotes) { section in
                Section(header: Text(section.id)) {
                    ForEach(section, id: \.self) { note in
                        VStack (alignment: .leading){
                            if let details = note.noteDetails {
                                Text(details)
                            }
                        }
                        .swipeActions {
                            Button(role: .destructive, action: {
                                delete(note: note)
                            }, label: {
                                Image(systemName: "trash")
                            })
                        }
                    }
                }
            }
        }
    }
    
    public func delete(note: Notes){
        
        viewContext.delete(note)
        do{
            try viewContext.save()
        } catch{
            print("delete note error = \(error)")
        }
    }
}

Calling the delete method from swipe-to-delete always crashes when the note is the last item on the list.

In the ContactDetailsView, and it's child view NotesListView I have marked the 'var contact: Contact' as an @ObservedObject. This is so that changes made to the contact can be reflected in the ContactDetailsView subviews (the name fields here, but there could be more). If I make both of these properties let contact: Contact, I don't get a crash anymore! But then I lose the 'observability' of the Contact, and changes to the name won't reflect on the Text fields.

So it seems like something about @ObservedObject and using a List in its subviews is causing this problem, but I'm not sure why. Maybe the @ObservedObject first reloads its relationship and updates the view, and then the FetchRequest also reloads the List, causing a double-delete? But it surprisingly only happens for the last element in the list, and not otherwise.

Another option I considered was losing the @FetchRequest and using contact.notes collection to drive the list. But isn't that inefficient compared to a @FetchRequest, especially with sorting and filtering, and loading the list of 1000s of notes?

Any suggestions for a work-around are welcome.

The full crash looks something like this:

*** Assertion failure in -[SwiftUI.UpdateCoalescingCollectionView _endItemAnimationsWithInvalidationContext:tentativelyForReordering:animator:collectionViewAnimator:], UICollectionView.m:10643
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'attempt to delete item 17 from section 0 which only contains 17 items before the update'
*** First throw call stack:
(
	0   CoreFoundation                      0x0000000180491128 __exceptionPreprocess + 172
	1   libobjc.A.dylib                     0x000000018008412c objc_exception_throw + 56
	2   Foundation                          0x0000000180d1163c _userInfoForFileAndLine + 0
	3   UIKitCore                           0x0000000184a57664 -[UICollectionView _endItemAnimationsWithInvalidationContext:tentativelyForReordering:animator:collectionViewAnimator:] + 4020
	4   UIKitCore                           0x0000000184a62938 -[UICollectionView _performBatchUpdates:completion:invalidationContext:tentativelyForReordering:animator:animationHandler:] + 388
	5   SwiftUI                             0x00000001c51f0c88 OUTLINED_FUNCTION_249 + 5648
....
	39  TestCoreDataSwiftUISections         0x000000010248c85c $s27TestCoreDataSwiftUISections0abcdE3AppV5$mainyyFZ + 40
	40  TestCoreDataSwiftUISections         0x000000010248c998 main + 12
	41  dyld                                0x0000000102625544 start_sim + 20
	42  ???                                 0x00000001027560e0 0x0 + 4336214240
	43  ???                                 0x2103000000000000 0x0 + 2378745028181753856

Replies

I managed to avert the problem by adding DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { ... before deleting the object and saving the context. I guess that gives the underlying view time to coalesce all the updates properly. Still feels like a little hacky, so would be great if there was a better solution to this.

Building on zulfishah's solution, just executing the button closure in an async context fixes the issue for me.

Button(role: .destructive) {
    Task {
        deleteObject()
    }
} label: {
    Label("Delete", systemImage: "trash.fill")
}