删除coredata列表项时SwiftUI App崩溃EXC_BAD_ACCESS错误

Posted

技术标签:

【中文标题】删除coredata列表项时SwiftUI App崩溃EXC_BAD_ACCESS错误【英文标题】:SwiftUI App Crashing EXC_BAD_ACCESS error when coredata list items are deleted 【发布时间】:2020-05-30 05:58:46 【问题描述】:

以下代码一直有效,直到对列表项执行删除操作。但这并不是每次代码运行时都会发生的。它因EXC_BAD_ACCESS 线程错误代码而崩溃:

然后我意识到,如果应用程序保持运行,删除操作后一定时间后会出现此错误

这就是为什么这个错误很难弄清楚的原因。但是当我在Model.swift 中将DispatchQueue.main.async 方法添加到tasks 时,它开始出现。

此代码的目的是当列表发生任何更改时,使用self.fetchAll() 方法从核心数据重新加载更新的结果。

我注意到的另一个问题是删除后出现的红线。

问题:

    更新核心数据列表时如何用最少的代码更新内容视图结构? (这个link 有一个不同的方法,每次在代码中显式调用 fetch 方法。) 如何优化这段代码,代码更少,稳定性更高? 图片中的红线问题是否是现有 macOS 错误的一部分?

macOS:10.15.4

XCode:11.5

目标:10.15

Model.swift

import Foundation
import CoreData

class Model: ObservableObject 
    @Published var context: NSManagedObjectContext
    @Published var tasks: [Task] = [Task]() 
        didSet 
            DispatchQueue.main.async 
                self.fetchAll()
            
        
    
    init(_ viewContext: NSManagedObjectContext) 
        context = viewContext
    


// MARK: Methods

extension Model 
    func fetchAll() 
        let req = Task.fetchRequest() as NSFetchRequest<Task>
        req.sortDescriptors = [NSSortDescriptor(keyPath: \Task.name, ascending: true)]
        do 
            self.tasks = try self.context.fetch(req)
         catch let error as NSError 
            print(error.localizedDescription)
        
    
    
    func addTask(_ text: String) 
        let name = text.trimmingCharacters(in: .whitespacesAndNewlines)
        if (name != "") 
            let task = Task(context: self.context)
            task.id = UUID()
            task.name = name
            task.creationTimestamp = Date()
            task.updatedTimestamp = Date()
            self.context.insert(task)
            self.save()
        
    
    
    func save() 
        guard self.context.hasChanges else  return 
        do 
            try self.context.save()
            print("Saved changes")
         catch let error as NSError 
            print(error.localizedDescription)
        
    

AppDelegate.swift

import Cocoa
import SwiftUI

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate 

    var window: NSWindow!
    var model: Model!
    
    func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool 
        return true
    
    
    func applicationDidFinishLaunching(_ aNotification: Notification) 
        
        model = Model(persistentContainer.viewContext)
        let contentView = ContentView().environmentObject(model)

        window = NSWindow(
            contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
            styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
            backing: .buffered, defer: false)
        window.center()
        window.setFrameAutosaveName("Main Window")
        window.contentView = NSHostingView(rootView: contentView)
        window.makeKeyAndOrderFront(nil)
    

    func applicationWillTerminate(_ aNotification: Notification)  

    // MARK: - Core Data stack

    lazy var persistentContainer: NSPersistentCloudKitContainer = 
        let container = NSPersistentCloudKitContainer(name: "Todo_List")
        container.loadPersistentStores(completionHandler:  (storeDescription, error) in
            if let error = error 
                fatalError("Unresolved error \(error)")
            
        )
        container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
        container.viewContext.undoManager = nil
        container.viewContext.shouldDeleteInaccessibleFaults = true
        container.viewContext.automaticallyMergesChangesFromParent = true
        return container
    ()

    // MARK: - Core Data Saving and Undo support

    @IBAction func saveAction(_ sender: AnyObject?) 
        let context = persistentContainer.viewContext

        if !context.commitEditing() 
            NSLog("\(NSStringFromClass(type(of: self))) unable to commit editing before saving")
        
        if context.hasChanges 
            do 
                try context.save()
             catch 
                let nserror = error as NSError
                NSApplication.shared.presentError(nserror)
            
        
    

    func windowWillReturnUndoManager(window: NSWindow) -> UndoManager? 
        return persistentContainer.viewContext.undoManager
    

    func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply 
        let context = persistentContainer.viewContext
        
        if !context.commitEditing() 
            NSLog("\(NSStringFromClass(type(of: self))) unable to commit editing to terminate")
            return .terminateCancel
        
        
        if !context.hasChanges 
            return .terminateNow
        
        
        do 
            try context.save()
         catch 
            let nserror = error as NSError
            let result = sender.presentError(nserror)
            if (result) 
                return .terminateCancel
            
            
            let question = NSLocalizedString("Could not save changes while quitting. Quit anyway?", comment: "Quit without saves error question message")
            let info = NSLocalizedString("Quitting now will lose any changes you have made since the last successful save", comment: "Quit without saves error question info");
            let quitButton = NSLocalizedString("Quit anyway", comment: "Quit anyway button title")
            let cancelButton = NSLocalizedString("Cancel", comment: "Cancel button title")
            let alert = NSAlert()
            alert.messageText = question
            alert.informativeText = info
            alert.addButton(withTitle: quitButton)
            alert.addButton(withTitle: cancelButton)
            
            let answer = alert.runModal()
            if answer == .alertSecondButtonReturn 
                return .terminateCancel
            
        
        return .terminateNow
    


ContentView.swift

import SwiftUI

struct ContentView: View 
    @EnvironmentObject var model: Model
    @State var taskName: String = ""
    
    var body: some View 
        VStack
            ZStack
                TaskList()
                    .padding(.top,31)
                VStack(spacing:0)
                    TextField("New Task", text: self.$taskName, onCommit: 
                        self.model.addTask(self.taskName)
                        self.taskName = ""
                    )
                        .padding(5)
                    Divider().offset(y:-1)
                        Spacer()
                
                if model.tasks.isEmpty 
                    Text("Nothing To Do\nPlease add something To Do.")
                        .multilineTextAlignment(.center)
                        .font(.headline)
                        .padding(.horizontal)
                        .frame(maxWidth: .infinity, maxHeight: .infinity)
                
            
        
        .frame(minWidth: 200, maxWidth: .infinity, maxHeight: .infinity)
        .onAppear
            self.model.fetchAll()
        
    

TaskList.swift

import SwiftUI

struct TaskList: View 
    @EnvironmentObject var model: Model
    
    @State var selection: Task?
    
    var body: some View 
        List(selection: self.$selection)
            ForEach(self.model.tasks, id: \.self)  task in
                TaskRow(task: task).tag(task)
            
            .onDelete(perform: onDelete)
        
    
    
    private func onDelete(with indexSet: IndexSet) 
        indexSet.forEach  index in
            let task = self.model.tasks[index]
            self.model.context.delete(task)
        
        self.model.save()
    

TaskRow.swift

struct TaskRow: View 
    @ObservedObject var task: Task
    @EnvironmentObject var model: Model
    var dateString: String 
        if let timestamp = task.updatedTimestamp 
            let formatter = DateFormatter()
            formatter.dateStyle = .long
            formatter.timeStyle = .medium
            return formatter.string(from: timestamp)
         else 
            return "No date recorded"
        
    
    var body: some View 
        HStack 
            Toggle(isOn: Binding<Bool>(get:  () -> Bool in
                return self.task.checked
            , set:  (enabled) in
                self.task.checked = enabled
                self.task.updatedTimestamp = Date()
                self.model.save()
            ))
                Text("")
            
            VStack 
                HStack 
                    Text(task.name ?? "Unknown Task").font(.system(size: 20))
                    Spacer()
                
                HStack 
                    Text(dateString).font(.system(size: 10)).bold()
                    Spacer()
                
            
        
    

【问题讨论】:

如果您说崩溃仅在删除后出现几次,它可能与 iCloud 同步有关,因为您使用的是 CloudContainer 【参考方案1】:

确保Task 是可识别的:

extension Task: Identifiable 

如果您不知道应该可以在您的ForEach 中执行此操作:

ForEach(self.model.tasks) task in

其实我也遇到过这个问题。我必须做的解决方法是添加一个字段isDeleted,而不是实际删除CoreData 中的记录。然后,您可以稍后在应用程序处于后台或终止时清理它。或者只是将记录保存在 Coredata 中。

【讨论】:

试过了。但仍然以同样的方式崩溃。 我不得不将阀门 contactIsDeleted 作为 Bool 添加到我的 CoreData 模型中,并由于删除联系人时列表崩溃而打开和关闭它。我将列表更改为 if !$0.contact_isDeleted UserRow(user: $0, property: $0.contactID) 我尝试添加 property: property,正如我在另一篇文章中看到的那样,但这仅在列表单元格当时在屏幕上不可见时才有帮助。【参考方案2】:

由于我最近有类似的问题可以解决,我想我会试试你的例子。我不确定您的问题是否仍然存在,因为这个问题已经 5 个月大了还没有得到回答。

简而言之:是的,您可以修复它。但是你必须使用 SwiftUI 而不是反对它。这意味着:您的模型可以提供获取请求,但您不应该自己执行获取。让 SwiftUI 为您处理。

以下是所需的更改:

    AppDelegate.swift

您应该在创建 ContentView 时将托管对象上下文添加到 SwiftUI 环境中:

let contentView = ContentView()
   .environmentObject(model)
   .environment(\.managedObjectContext, persistentContainer.viewContext)
    Model.swift

删除您的 tasks 已发布属性和 fetchAll 函数。我们会让 SwiftUI 处理这个。

    内容视图

删除对model.fetchAll() 的调用和对model.tasks.isEmpty 的检查,因为 ContentView 对任务一无所知。

    任务列表

添加FetchRequest 以创建您的tasks 属性并在此处添加tasks.isEmpty 的检查。代码如下:

struct TaskList: View 
    @EnvironmentObject var model: Model

    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \Task.name, ascending: true)],
        animation: .default)
    private var tasks: FetchedResults<Task>

    @State var selection: Task?

    @ViewBuilder
    var body: some View 
        if tasks.isEmpty 
            Text("Nothing To Do\nPlease add something To Do.")
                .multilineTextAlignment(.center)
                .font(.headline)
                .padding(.horizontal)
                .frame(maxWidth: .infinity, maxHeight: .infinity)
        
        else 
            List(selection: $selection)
                ForEach(tasks, id: \.id)  task in
                    TaskRow(task: task).tag(task)
                
                .onDelete(perform: onDelete)
            
        
    

    private func onDelete(with indexSet: IndexSet) 
        indexSet.forEach  index in
            let task = tasks[index]
            self.model.context.delete(task)
        
        self.model.save()
    

使用这种方法确实对我很有效。 此外,删除动画现在正在正常工作和绘制。

【讨论】:

以上是关于删除coredata列表项时SwiftUI App崩溃EXC_BAD_ACCESS错误的主要内容,如果未能解决你的问题,请参考以下文章

SwiftUI - 如何在 CoreData 中删除实体中的行

浅谈CoreData海量数据(实时动态添加和删除)在SwiftUI列表中分页显示的原理及实现

SwiftUI 2.0列表项层级缩进显示CoreData托管数据的3种实现

SwiftUI 2.0列表项层级缩进显示CoreData托管数据的3种实现

SwiftUI 列表在删除后继续显示核心数据项

无法在 SwiftUI 中使用 ForEach 和 CoreData 将数据正确传递给模态演示