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