首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >onDelete致NSRangeException

onDelete致NSRangeException
EN

Stack Overflow用户
提问于 2022-04-24 21:29:54
回答 1查看 241关注 0票数 0

在这个应用程序中,有一个主屏幕(WorkoutScreen),当它遍历列表时,它一次只显示一个列表的内容(当前在多个列表中的锻炼)。在popOver中,会出现包含所有锻炼的列表,并且能够在该列表中添加、删除或移动项目。

当我删除最下面的项目时,没有错误。当我删除列表中的任何其他项目时,我会得到这个NSRangeException错误,它会使应用程序崩溃:

代码语言:javascript
复制
/*
    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)
    */
代码语言:javascript
复制
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
    }
}

当我将一项添加到列表中时,我会得到错误:‘’CoreData:警告:多个NSEntityDescriptions声称NSManagedObject子类'FruitEntity‘so +实体无法消除歧义。

在不添加或删除任何优先项的情况下移动项时,在关闭弹出时会出现此错误:“错误保存: error Domain=NSCocoaErrorDomain Code=133020”无法合并更改。UserInfo={conflictList=( NSMergeConflict (0x2804f1480)用于NSManagedObject (0x28327d900)和objectID‘0x9ede5774e26501a4.’)

以下是核心数据和相关功能:

代码语言:javascript
复制
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
        }
    }

以下是列表结构:

代码语言:javascript
复制
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()
    }
}

这是EditWorkouts文件连接到的WorkoutPopUp文件:

代码语言:javascript
复制
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()
               //}
            }
        }
    }
}

CoreData FruitEntity:图像图片

再次感谢您的时间!!

EN

回答 1

Stack Overflow用户

回答已采纳

发布于 2022-04-25 01:03:29

您的代码有几个问题。我怀疑其中一个是造成这次崩盘的唯一原因,但另一个可能也是原因之一。首先,最有可能的罪魁祸首。如果使用.onDelete(),就不能使用id: \.self。原因很简单:ForEach可能会对哪个实体感到非常困惑。.self通常并不是唯一的,如果您要删除和重新排列ForEach()中的内容,即.onDelete().onMove(),则确实需要这样做。

解决办法很简单。您在ForEach中使用的任何东西都应该符合Identifiable。核心数据托管对象都符合Identifiable,所以修复很容易;删除‘`id:.self’

代码语言:javascript
复制
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")
                }
            }
        }
    }
}

光靠这个解决方案很有可能就能阻止这场危机。但是,我也注意到您的更新在您的视图中有问题。这是因为您没有实现NSFetchedResultsControllerNSFetchedResultsControllerDelegate,后者在核心数据存储更改时更新数组。视图模型应该如下所示:

代码语言:javascript
复制
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
    }
}

您将注意到视图中不再存在refreshID。它不用它就能更新。另外,请注意,通过将数据存储init集成到视图模型中,您不能将其扩展到具有其他视图的其他实体。每个程序都会有不同的上下文,它们会使应用程序崩溃。最好是拥有一个控制器类来为Core数据存储创建一个单例,比如Apple在默认设置中给您提供了什么。

最后,我认为您的问题是将使用id: .self (众所周知会与.onDelete()崩溃)和使用refreshID而不是NSFetchedResultsController更新List结合在一起。

票数 0
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/71992483

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档