SwiftUI Core Data Cannot Delete Multiple Rows from List

Viewed 266

I have a SwiftUI app with full SwiftUI flow with Core Data persistence. It is a basic master/detail style and it solidly works as expected. I want to add a feature to allow multiple rows to be selected and deleted. Single row swipe to delete works as expected.

I had inspiration from this SO: 58910010 however, that example used an array of struct items and I have not been able to duplicate the procedure with Core Data objects.

Basically, the functionality of presenting edit check boxes and the button toggling functions work as expected, but when I tap the Trash icon, the app crashes. There is no information in the console and nothing of help to me in the thread log.

See Note 1 below in deleteCards(). If I comment out the viewContext save, the app does not crash and the selected cards are removed from the list but on restart, the cards still exist. If I leave the viewContext save in place, the app crashes, but on restart I see that the selected items no longer exist.

Here's a simplified version:

struct ContentView: View {
    @Environment(\.managedObjectContext) private var viewContext
    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \Card.lastName, ascending: true)],
        animation: .default)
    private var cards: FetchedResults<Card>
    @State var editMode: EditMode = .inactive
    @State var selection = Set<UUID>()

    var body: some View {
        NavigationView {
            List(selection: $selection) {
                ForEach(cards) { card in
                    VStack(alignment: .leading) {
                        Text("\(card.wrappedFirstName) \(card.wrappedLastName)")
                        Text("\(card.wrappedComment)")
                    }
                }
                .onDelete(perform: deleteItems)
            }
            .navigationBarItems(
                leading: editButton,
                trailing:
                    HStack {
                        addDelButton
                        addExamplesButton
                    }
            )
            .environment(\.editMode, self.$editMode)
            .navigationTitle("Cards")
        }//nav
    }//body

    private var editButton: some View {
        Button(action: {
            self.editMode.toggle()
            self.selection = Set<UUID>()
        }) {
            Text(self.editMode.title)
        }
    }//edit button

    private var addDelButton: some View {
        if editMode == .inactive {
            return Button(action: addCard) {
                Image(systemName: "plus")
            }
        } else {
            return Button(action: deleteCards) {
                Image(systemName: "trash")
            }
        }
    }//add del button

    private var addExamplesButton: some View {
        Button(action: {
            self.addExampleRecords()
        }) {
            Image(systemName: "gear")
        }
    }//add examples

    private func deleteCards() {
        for id in selection {
            if let index = cards.lastIndex(where: { $0.id == id }) {
                viewContext.delete(cards[index])
            }
        }
        selection = Set<UUID>()
    
        //Note 1: this crashed the app - but deletions happen
        do {
            try viewContext.save()
        } catch {
            // Replace this
            let nsError = error as NSError
            fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
        }
    }//delete Cards

    private func addCard() {
        withAnimation {
            let newCard = Card(context: viewContext)
            newCard.id = UUID()
            newCard.firstName = "NewFirstName"
            newCard.lastName = "NewLastName"
            newCard.creationDate = Date()

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

    private func addExampleRecords() {
    
        let context = self.viewContext
        for c in 0..<10 {
            let nmo = Card(context: context)
            nmo.id = UUID()
            nmo.lastName = "ExLast \(c)"
            nmo.firstName = "ExFirst \(c)"
            nmo.comment = "ExComment \(c)"
            nmo.creationDate = Date()
        }
    }//add example records

    private func deleteItems(offsets: IndexSet) {
        withAnimation {
            offsets.map { cards[$0] }.forEach(viewContext.delete)

            do {
                try viewContext.save()
            } catch {
                // Replace this
                let nsError = error as NSError
                fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
            }
        }
    }//delete items
}//struct

extension EditMode {
    var title: String {
        self == .active ? "Done" : "Edit"
    }
    mutating func toggle() {
        self = self == .active ? .inactive : .active
    }
}// ext

And for completeness:

extension Card {

    @nonobjc public class func fetchRequest() -> NSFetchRequest<Card> {
        return NSFetchRequest<Card>(entityName: "Card")
    }

    @NSManaged public var id: UUID
    @NSManaged public var firstName: String?
    @NSManaged public var comment: String?
    @NSManaged public var lastName: String?
    @NSManaged public var creationDate: Date?

    public var wrappedFirstName: String {
        firstName ?? "no first name"
    }

    public var wrappedLastName: String {
        lastName ?? "no last name"
    }

    public var wrappedComment: String {
        comment ?? "no comment"
    }

    public var wrappedCreationDate: Date {
        creationDate ?? Date()
    }

}

extension Card : Identifiable {

}   

Any guidance would be appreciated. Xcode 12.3 iOS 14.2

0 Answers
Related