[英]onDelete causing NSRangeException
In this app, there is a main screen (WorkoutScreen) that displays the contents of a list one at a time as it iterates through the list (current workout in a list of many).在这个应用程序中,有一个主屏幕 (WorkoutScreen),它在遍历列表时一次显示一个列表的内容(当前锻炼在许多列表中)。 In a popOver, a list that contains all of the workouts appears and has the ability to add, delete or move items in that list.在弹出窗口中,会出现一个包含所有锻炼的列表,并且可以在该列表中添加、删除或移动项目。
When I delete the bottom most item, there is no error.当我删除最底部的项目时,没有错误。 When I delete any other item in the list I get this NSRangeException error that crashes the app:当我删除列表中的任何其他项目时,我会收到导致应用程序崩溃的 NSRangeException 错误:
/*
2022-04-24 15:41:21.874306-0400 Trellis
beta[9560:3067012] *** Terminating app due to
uncaught exception 'NSRangeException', reason:
'*** __boundsFail: index 3 beyond bounds [0 ..
2]'
*** First throw call stack:
(0x1809150fc 0x19914fd64 0x180a1e564 0x180a2588c
0x1808c0444 0x1852dcce4 0x1852e1400 0x185424670
0x185423df0 0x185428a40 0x18843e4a0 0x188510458
0x188fd83ec 0x10102f3bc 0x1010500a4 0x188494f4c
0x10102c664 0x10103e0d4 0x18841a944 0x10102be18
0x10103122c 0x18837b8ac 0x188363484 0x18834bb64
0x188371d20 0x1883b88e4 0x1b28fe910 0x1b28fe318
0x1b28fd160 0x18831e780 0x18832f3cc 0x1883f5e34
0x18834206c 0x188345f00 0x182eb0798 0x184613138
0x184605958 0x184619f80 0x184622874 0x1846050b0
0x183266cc0 0x1835015fc 0x183b7d5b0 0x183b7cba0
0x1809370d0 0x180947d90 0x180882098 0x1808878a4
0x18089b468 0x19c42638c 0x18323d088 0x182fbb958
0x1885547a4 0x188483928 0x1884650c0 0x10109a630
0x10109a700 0x1015b9aa4)
libc++abi: terminating with uncaught exception
of type NSException
dyld4 config:
DYLD_LIBRARY_PATH=/usr/lib/system/introspection
DYLD_INSERT_LIBRARIES=/Developer/usr/lib/libBacktrac
eRecording.dylib:/Developer/usr/lib/libMainThreadChecker.dylib:/Developer/Library/Private
Frameworks/DTDDISupport.framework/libViewDebuggerSupport.dylib
*** Terminating app due to uncaught exception 'NSRangeException', reason: '***
__boundsFail: index 3 beyond bounds [0 .. 2]'
terminating with uncaught exception of type NSException
(lldb)
*/
struct WorkoutScreen: View {
@EnvironmentObject var workoutList: CoreDataViewModel //calls it from environment
@StateObject var vm = CoreDataViewModel() //access on the page
@Environment(\.scenePhase) var scenePhase
var body: some View{
//displays the current item in the list
}
}
When I add an item to the list I get the error: ''' CoreData: warning: Multiple NSEntityDescriptions claim the NSManagedObject subclass 'FruitEntity' so +entity is unable to disambiguate." '''当我将一个项目添加到列表中时,我收到错误:'''CoreData:警告:多个 NSEntityDescriptions 声明 NSManagedObject 子类'FruitEntity',因此 +entity 无法消除歧义。“'''
Moving Items without adding or deleting any prior gives me this error upon closing the pop over: ''' Error saving: Error Domain=NSCocoaErrorDomain Code=133020 "Could not merge changes."移动项目而不添加或删除任何先前在关闭弹出窗口时会出现此错误:''' 保存错误:错误域 = NSCocoaErrorDomain 代码 = 133020“无法合并更改。” UserInfo={conflictList=( "NSMergeConflict (0x2804f1480) for NSManagedObject (0x28327d900) with objectID '0x9ede5774e26501a4... ''' UserInfo={conflictList=("NSManagedObject (0x28327d900) 的 NSMergeConflict (0x2804f1480),objectID 为 '0x9ede5774e26501a4...'''
Here is the core data and related functions:下面是核心数据和相关函数:
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
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)")
}
}
func addFruit(text: String, nummSets: Int16, nummWeights: Int16, nummReps: Int16, secOrRepz: Bool, orderNumz: Int64, multilimbz: Bool, countDownz: Int16, repTimez: Int16, restTimez: Int16, circuitz: Bool) {
let newFruit = FruitEntity(context: container.viewContext)
newFruit.name = text
newFruit.numOFSets = nummSets
newFruit.numOFWeight = nummWeights
newFruit.numOFReps = nummReps
newFruit.measure = secOrRepz
newFruit.order = orderNumz
newFruit.multiLimb = multilimbz
newFruit.countDownSec = countDownz
newFruit.timePerRep = repTimez
newFruit.restTime = restTimez
newFruit.circuit = circuitz
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()
fetchFruits()
} 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
}
}
Here is the list struct:这是列表结构:
struct WorkoutListPopUp: View {
@ObservedObject var vm = CoreDataViewModel()
@EnvironmentObject var listViewModel: ListViewModel
@EnvironmentObject var workoutList: CoreDataViewModel
//Too many @State var to list here
var body: some View {
Button (action: {
//this triggers the bug>
vm.addFruit(text: "Workout name", nummSets: Int16(addSets) ?? 3, nummWeights: Int16(addWeights) ?? 0, nummReps: Int16(addReps) ?? 8, secOrRepz: addSecOrReps, orderNumz: Int64((vm.savedEntities.count)), multilimbz: dualLimbs, countDownz: 10, repTimez: 3, restTimez: 60, circuitz: false)
loadNums()
}, label: {
Image(systemName: "plus")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width:20, height: 20)
.foregroundColor(Color.pink.opacity(1.0))
.padding(.top, 0)
})
List(){
ForEach(vm.savedEntities) {entity in
VStack{
EditWorkouts(entity: entity, prescribeMeasure: $prescribeMeasure, addReps: $addReps, measurePrescribed: $measurePrescribed, repTimePicker: $repTimePicker, repz: $repz, restPicker: $restPicker, setz: $setz, ready2Press: $ready2Press, workoutz: $workoutz, weightz: $weightz, setsRemaining: $setsRemaining, workoutNum: $workoutNum, workoutInstructions: $workoutInstructions, multiplelimbs: $multiplelimbs, showAllInfo: $showAllInfo)
//are these onChanges needed if "EditWorkouts" file is saving?
.onChange(of: entity.name) { text in
vm.saveData()
loadNums()
}
.onChange(of: entity.numOFSets) { text in
vm.saveData()
loadNums()
}
.onChange(of: entity.numOFReps) { text in
vm.saveData()
loadNums()
}
.onChange(of: entity.numOFWeight) { text in
vm.saveData()
loadNums()
}
.onChange(of: entity.measure) { text in
vm.saveData()
loadNums()
}
.onChange(of: entity.order) { text in
vm.saveData()
loadNums()
}
.onChange(of: entity.circuit) { text in
vm.saveData()
loadNums()
}
}
}
.onDelete(perform: vm.deleteFunction)
.onMove(perform: moveItem)
}
}
func loadNums(){
if vm.savedEntities.count > 0 {
workoutz = vm.savedEntities[workoutNum].name ?? "NO Name"
setz = String(vm.savedEntities[workoutNum].numOFSets)
weightz = String(vm.savedEntities[workoutNum].numOFWeight)
repz = String(vm.savedEntities[workoutNum].numOFReps)
multiplelimbs = vm.savedEntities[workoutNum].multiLimb
prescribeMeasure = vm.savedEntities[workoutNum].measure
if setsRemaining == 0 && ((workoutNum + 1) - (Int(vm.savedEntities.count)) == 0) {
workoutInstructions = "Goal: \(repz) \(measurePrescribed)"
}
else {
workoutInstructions = "Goal: \(repz) \(measurePrescribed)"
}
}
else {
workoutz = "Add a Workout 👉"
workoutInstructions = " "
}
}
func moveItem(indexSet: IndexSet, destination: Int) {
let source = indexSet.first!
if destination > source {
var startIndex = source + 1
let endIndex = destination - 1
var startOrder = vm.savedEntities[source].order
while startIndex <= endIndex {
vm.savedEntities[startIndex].order = startOrder
startOrder = startOrder + 1
startIndex = startIndex + 1
}
vm.savedEntities[source].order = startOrder
}
else if destination < source {
var startIndex = destination
let endIndex = source - 1
var startOrder = vm.savedEntities[destination].order + 1
let newOrder = vm.savedEntities[destination].order
while startIndex <= endIndex {
vm.savedEntities[startIndex].order = startOrder
startOrder = startOrder + 1
startIndex = startIndex + 1
}
vm.savedEntities[source].order = newOrder
}
vm.savedEntities[source].circuit = false
vm.saveData()
loadNums()
}
}
This is the EditWorkouts file that the WorkoutPopUp file connects to:这是 WorkoutPopUp 文件连接到的 EditWorkouts 文件:
struct EditWorkouts: View {
@EnvironmentObject var workoutList: CoreDataViewModel
@StateObject var vm = CoreDataViewModel()
@EnvironmentObject var listViewModel: ListViewModel
let entity: FruitEntity
//too many @State vars to post
var body: some View {
VStack{
HStack{
//many lines of code for options that alter the respective workout on the list. All are followed by their version of:
//.onChange(of:
//vm.savedEntities[Int(entity.order)].multiLimb) { _ in
//vm.saveData()
//loadNums()"
//}
//-or-
//.onChange(of:vm.savedEntities[Int(entity.order)].circuit) { _ in
//entity.circuit = entity.circuit
//vm.saveData()
//}
}
}
}
}
Picture of CoreData FruitEntity: Image CoreData FruitEntity 图片:图片
Thank you again for your time!!再次感谢您的宝贵时间!!
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
.如果您使用.onDelete()
,则不能使用id: \.self
。 The reason is pretty simple: the ForEach
can get pretty confused as to which entity is which.原因很简单:对于哪个实体是哪个, ForEach
可能会很困惑。 .self
is often not unique, and it really needs to be if you are deleting and rearranging things in the ForEach()
, ie .onDelete()
and .onMove()
. .self
通常不是唯一的,如果您要删除和重新排列ForEach()
中的内容,即.onDelete()
和.onMove()
,它确实需要如此。
The solution is simple.解决方案很简单。 Whatever you are using in the ForEach
should conform to Identifiable
.无论您在ForEach
中使用什么,都应该符合Identifiable
。 Core Data managed objects all conform to Identifiable
, so the fix is easy; Core Data 托管对象都符合Identifiable
,因此修复很容易; remove the `id: .self``:删除`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.那是因为您没有实现NSFetchedResultsController
和NSFetchedResultsControllerDelegate
,它们会在您的核心数据存储更改时更新您的数组。 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.您会注意到视图中不再存在refreshID
。 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
.最后,我认为您的问题是使用id: .self
与.onDelete()
崩溃以及您使用refreshID
而不是NSFetchedResultsController
来更新List
的事实的组合。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.