Ondelete Causing Nsrangeexception

onDelete causing NSRangeException

There are a couple of issues with your code. I suspect one is the sole contributor to the crash, but the other may be contributing as well. First, the most likely culprit. If you use .onDelete(), you can't use id: \.self. The reason is pretty simple: the ForEach can get pretty confused as to which entity is which. .self is often not unique, and it really needs to be if you are deleting and rearranging things in the ForEach(), i.e. .onDelete() and .onMove().

The solution is simple. Whatever you are using in the ForEach should conform to Identifiable. Core Data managed objects all conform to Identifiable, so the fix is easy; remove the `id: .self``:

struct ListView: View {

@StateObject var vm = CoreDataViewModel()

var body: some View {
List {
ForEach(vm.savedEntities) {entity in
Text(entity.name ?? "")
}
.onDelete(perform: vm.deleteFunction)
}
// This just adds a button to create entities.
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
vm.addFruit()
} label: {
Image(systemName: "plus")
}
}
}
}
}

That fix alone will most likely stop the crash. However, I also noticed that you were having issues with your updates in your view. That is because you did not implement an NSFetchedResultsController and NSFetchedResultsControllerDelegate which updates your array when your Core Data store changes. Your view model should look like this:

import SwiftUI
import CoreData

class CoreDataViewModel: NSObject, ObservableObject {
private let container: NSPersistentContainer
private let context: NSManagedObjectContext
// Whenever you put your Core Data fetch in a view model, you should use an NSFetchedResultsController.
// This allows you to automatically update your @Published var when your Core Data store changes.
// You must inherit from NSObject to use it.
private let fetchResultsController: NSFetchedResultsController<FruitEntity>

@Published var savedEntities: [FruitEntity] = []

override init() {
container = NSPersistentContainer(name: "FruitsContainer")
container.loadPersistentStores { (description, error) in
if let error = error {
print("ERROR LOADING CORE DATA: \(error)")
}
else {
print("Successfully loaded core data")
}
}
context = container.viewContext
let request = NSFetchRequest<FruitEntity>(entityName: "FruitEntity")
let sort = NSSortDescriptor(keyPath: \FruitEntity.order, ascending: true)
request.sortDescriptors = [sort]
// This initializes the fetchResultsController
fetchResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)

// Because you inherit from NSObject, you must call super.init() to properly init the parent class. The order of when
// this is to be called has changed.
super.init()
// Because this is a delegate action, you must set the delegate. Since the view model will respond, we set the delegate to self.
fetchResultsController.delegate = self

// Renamed function to conform to naming conventions. You should use an active verb like fetch to start the name.
fetchFruits()
}

func fetchFruits() {

do {
// Instead of calling container.viewContext.fetch(request) which is static, use fetchResultsController.performFetch()
try fetchResultsController.performFetch()
// Make sure the fetch result is not nil
guard let fruitRequest = fetchResultsController.fetchedObjects else { return }
savedEntities = fruitRequest
// You do not need to let error. error is automatically captured in a do catch.
} catch {
print("Error fetching \(error)")
}
}

// This is just to be able to add some data to test.
func addFruit() {
var dateFormatter: DateFormatter {
let df = DateFormatter()
df.dateStyle = .short
return df
}
let fruit = FruitEntity(context: context)
fruit.name = dateFormatter.string(from: Date())
fruit.measure = false
fruit.numOfReps = 0
fruit.numOfSets = 0
fruit.numOfWeight = 0
fruit.order = 0
saveData()
}

func deleteFunction(indexSet: IndexSet) {
guard let index = indexSet.first else { return }
let entity = savedEntities[index]
container.viewContext.delete(entity)
saveData()
}

func saveData() {
do {
try context.save()
} catch let error {
print("Error saving: \(error)")
}
}
}

// This is your delegate extension that handles the updating when your Core Data Store changes.
extension CoreDataViewModel: NSFetchedResultsControllerDelegate {
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
// Essentially, you are redoing just the fetch as the NSFetchedResultsController knows how to fetch from above
guard let fruits = controller.fetchedObjects as? [FruitEntity] else { return }
self.savedEntities = fruits
}
}

You will notice refreshID no longer exists in the view. It updates without it. Also, please note that by incorporating the data store init into your view model, you can't expand it to have other entities with other views. Each will have a different context and they will crash the app. You are better off having a controller class that creates a singleton for the Core Data store, such as what Apple gives you in the default set up.

In the end, I think you issue was a combination of using id: .self which is known to crash with .onDelete() AND the fact that you were using refreshID not NSFetchedResultsController to update the List.

Core Data CloudKit - List not updating after done onMove sorting

I've managed to fix my issue by adding

.environment(\.editMode, self.$isEditMode)

to my List in the view where the Editbutton() is located.

With this variable @State var isEditMode: EditMode = .inactive

NSPersistentDocument FetchRequest warp property crash on macOS Document App SwiftUI project

This is due to emptiness of new document. As in any document-based application you have to prepare some default initial data for new document

Here is possible solution. Tested with Xcode 11.4 / iOS 13.4

in Document.swift

class Document: NSPersistentDocument {

// .. other code here

override func makeWindowControllers() {

// in case of new document create new empty book in context
// that will be shown in opened document window
let isNew = self.fileURL == nil
if isNew {
_ = Book(context: self.managedObjectContext!) // << here !!
}

let contentView = ContentView().environment(\.managedObjectContext, self.managedObjectContext!)

// ... other code here


Related Topics



Leave a reply



Submit