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