删除核心数据列表项时,SwiftUI应用程序崩溃EXC_BAD_ACCESS错误

时间:2020-05-30 05:58:46

标签: macos core-data swiftui runtime-error

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

ERROR

然后我意识到,如果应用程序保持运行,则在删除操作后的一段时间后会发生此错误

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

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

Red Line on Delete

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

问题:

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

macOS:10.15.4

XCode:11.5

目标:10.15

Model.swift

self.fetchAll()

AppDelegate.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)
        }
    }
}

ContentView.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
    }
}

TaskList.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()
        }
    }
}

TaskRow.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()
    }
}

2 个答案:

答案 0 :(得分:0)

确保Task是可识别的:

extension Task: Identifiable {
}

如果您不知道自己应该可以在ForEach中这样做:

ForEach(self.model.tasks) {task in
}

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

答案 1 :(得分:0)

由于我最近有类似的问题可以解决,因此我想尝试一下您的示例。我不确定您的问题是否仍然存在,因为该问题已经存在5个月了,没有得到答复。

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

以下是必需的更改:

  1. AppDelegate.swift

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

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

删除您发布的tasks属性和fetchAll函数。我们将让SwiftUI处理此问题。

  1. ContentView

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

  1. 任务列表

添加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()
    }
}

使用这种方法确实对我有用。 另外,删除动画现在可以正常工作并可以绘制。