Greetings - The following code appears to work, but when a list item is deleted from a Category section that contains other list items, the app crashes (error = "Thread 1: EXC_BREAKPOINT (code=1, subcode=0x180965354)"). I've confirmed that the intended item is deleted from the appData.items array - the crash appears to happen right after the item is deleted.
I suspect that the problem somehow involves the fact that the AppData groupedByCategory dictionary and sortedByCategory array are computed properties and perhaps not updating as intended when an item is deleted? Or maybe the ContentView doesn't know they've been updated? My attempt to solve this by adding "appData.objectWillChange.send()" has not been successful, nor has my online search for solutions to this problem.
I'm hoping someone here will either know what's happening or know I could look for additional solutions to try. My apologies for all of the code - I wanted to include the three files most likely to be the source of the problem. Length restrictions prevent me from including the "AddNewView" code and some other details, but just say the word if that detail would be helpful.
Many, many thanks for any help anyone can provide!
@main
struct DeletionCrashApp: App {
let persistenceController = PersistenceController.shared
// Not sure where I should perform this command
@StateObject var appData = AppData(viewContext: PersistenceController.shared.container.viewContext)
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
.environmentObject(appData)
}
}
}
import Foundation
import SwiftUI
import CoreData
class AppData: NSObject, ObservableObject {
// MARK: - Properties
@Published var items: [Item] = []
private var fetchedResultsController: NSFetchedResultsController<Item>
private (set) var viewContext: NSManagedObjectContext // viewContext can be read but not set from outside this class
// Create a dictionary based upon the category
var groupedByCategory: [String: [Item]] {
Dictionary(grouping: items.sorted(), by: {$0.category})
}
// Sort the category-based dictionary alphabetically
var sortedByCategoryHeaders: [String] {
groupedByCategory.map({ $0.key }).sorted(by: {$0 < $1})
}
// MARK: - Methods
func deleteItem(itemObjectID: NSManagedObjectID) {
do {
guard let itemToDelete = try viewContext.existingObject(with: itemObjectID) as? Item else {
return // exit the code without continuing or throwing an error
}
viewContext.delete(itemToDelete)
} catch {
print("Problem in the first do-catch code: \(error)")
}
do {
try viewContext.save()
} catch {
print("Failure to save context: \(error)")
}
}
// MARK: - Life Cycle
init(viewContext: NSManagedObjectContext) {
self.viewContext = viewContext
let request = NSFetchRequest<Item>(entityName: "ItemEntity")
request.sortDescriptors = [NSSortDescriptor(keyPath: \Item.name, ascending: true)]
fetchedResultsController = NSFetchedResultsController(fetchRequest: request,
managedObjectContext: viewContext,
sectionNameKeyPath: nil,
cacheName: nil)
super.init()
fetchedResultsController.delegate = self
do {
try fetchedResultsController.performFetch()
guard let items = fetchedResultsController.fetchedObjects else {
return
}
self.items = items
} catch {
print("failed to fetch items: \(error)")
}
} // end of init()
} // End of AppData
extension AppData: NSFetchedResultsControllerDelegate {
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
guard let items = controller.fetchedObjects as? [Item] else { return }
self.items = items
}
}
import SwiftUI
import CoreData
struct ContentView: View {
@Environment(\.managedObjectContext) private var viewContext
@EnvironmentObject var appData: AppData
@State private var showingAddNewView = false
@State private var itemToDelete: Item?
@State private var itemToDeleteObjectID: NSManagedObjectID?
var body: some View {
NavigationView {
List {
ForEach(appData.sortedByCategoryHeaders, id: \.self) { categoryHeader in
Section(header: Text(categoryHeader)) {
ForEach(appData.groupedByCategory[categoryHeader] ?? []) { item in
Text(item.name)
.swipeActions(allowsFullSwipe: false) {
Button(role: .destructive) {
self.itemToDelete = appData.items.first(where: {$0.id == item.id})
self.itemToDeleteObjectID = itemToDelete!.objectID
appData.deleteItem(itemObjectID: itemToDeleteObjectID!)
// appData.objectWillChange.send() <- does NOT fix the fatal crash
} label: {
Label("Delete", systemImage: "trash.fill")
}
} // End of .swipeActions()
} // End of ForEach(appData.groupedByReplacementCategory[categoryHeader]
} // End of Section(header: Text(categoryHeader)
} // End of ForEach(appData.sortedByCategoryHeaders, id: \.self)
} // End of List
.navigationBarTitle("", displayMode: .inline)
.navigationBarItems(
trailing:
Button(action: {
self.showingAddNewView = true
}) {
Image(systemName: "plus")
}
)
.sheet(isPresented: $showingAddNewView) {
// show AddNewView here
AddNewView(name: "")
}
} // End of NavigationView
} // End of body
} // End of ContentView
extension Item {
@nonobjc public class func fetchRequest() -> NSFetchRequest<Item> {
return NSFetchRequest<Item>(entityName: "ItemEntity")
}
@NSManaged public var category: String
@NSManaged public var id: UUID
@NSManaged public var name: String
}
extension Item : Identifiable {
}