JohnSF
JohnSF

Reputation: 4300

SwiftUI CoreData Filtered List Deletion Fails Unpredictably

I'm struggling with a SwiftUI app with SwiftUI life cycle where I have created a fairly standard Core Data list and have added a search field to filter the list. Two views - one with a predicate and one without. The unfiltered list generation works as expected including swipe to delete. The filtered list appropriately displays the filtered list, but on swipe to delete I get completely unpredictable results. Sometimes the item disappears, sometimes it pops back on the list, sometimes the wrong item is deleted. I cannot discern any pattern.

Here's the view:

struct MyFilteredListView: View {
    @Environment(\.managedObjectContext) private var viewContext
    @Environment(\.horizontalSizeClass) var sizeClass

    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \InvItem.name, ascending: true)],
        animation: .default) private var invItems: FetchedResults<InvItem>

    var fetchRequest: FetchRequest<InvItem>

    init(filter: String) {
        //there are actually a bunch of string fields included - just listed two here
        fetchRequest = FetchRequest<InvItem>(entity: InvItem.entity(), sortDescriptors: [], predicate: NSPredicate(format: "category1 CONTAINS[c] %@ || name CONTAINS[c] %@   ", filter, filter))
    }

    var body: some View {
    
        let sc = (sizeClass == .compact)
    
        return List {
            ForEach(fetchRequest.wrappedValue, id: \.self) { item in
                NavigationLink(destination: InvItemDetailView(invItem: item)) {
                    InvItemRowView(invItem: item)
                        .frame(minWidth: 0, maxWidth: .infinity)
                        .frame(height: sc ? 100 : 200)
                        .padding(.leading, 10)
                }//link
            }
            .onDelete(perform: deleteInvItems)

        }//list
    }

    private func deleteInvItems(offsets: IndexSet) {
        withAnimation {

            offsets.map { invItems[$0] }.forEach(viewContext.delete)

            do {
                try viewContext.save()
            } catch {
                // Replace this - raise an alert
                let nsError = error as NSError
                fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
            }
        }
    }
}

Core Data is pretty standard:

class InvItem: NSManagedObject, Identifiable {}

extension InvItem {
    @NSManaged var id: UUID
    @NSManaged var category1: String?
    @NSManaged var name: String
    //bunch more attributes

    public var wrappedCategory1: String {
        category1 ?? "No category1"
    }
    //other wrapped items
}//extension inv item

extension InvItem {
    static func getAllInvItems() -> NSFetchRequest<InvItem> {
        let request: NSFetchRequest<InvItem> = InvItem.fetchRequest() as! NSFetchRequest<InvItem>
        let sortDescriptor = NSSortDescriptor(key: "name", ascending: true)
        request.sortDescriptors = [sortDescriptor]
        return request
    }
}//extension

I'm guessing there is something about the offsets behavior in deleteInvItems(offsets: IndexSet) that I don't understand, but I have not been able to find anything on this. This same code works as expected in the unfiltered list.

Any guidance would be appreciated. Xcode Version 12.2 beta (12B5018i) iOS 14

First Edit: I figured out the pattern. Apparently the IndexSet refers to the entire entity, not the filtered items. Say for example that the unfiltered list has 10 items and the filtered list has 3 items. When I delete the third item in the filtered list the result is the deletion of the third item in the unfiltered list, so unless those two are the same, the third item reappears on the filtered list and the third item on the unfiltered list is deleted from Core Data. If I then delete the third item on the filtered list again, the fourth item on the original list is deleted (since the third is already gone). So the question becomes - how do I get a reference to the object I want to delete - IndexSet does not work.

Upvotes: 0

Views: 359

Answers (1)

lorem ipsum
lorem ipsum

Reputation: 29309

It is likely because your ForEach is using a wrappedValue which may or may not be updated.

https://developer.apple.com/documentation/swiftui/binding/wrappedvalue

I suggest you look at the code that is provided when you create a new project (you are using some of it) so you can adjust and have more accurate data.

ForEach(items) { item in

Also, when deleting you are mixing the offset from fetchRequest and items in invItems stick with only one.

If you want to be able to dynamically filter the list I have had better luck using a FetchedResultsController wrapped in an ObservedObject https://www.youtube.com/watch?v=-U-4Zon6dbE

Upvotes: 1

Related Questions