簡體   English   中英

SwiftUI 刪除 coredata 列表項時應用程序崩潰 EXC_BAD_ACCESS 錯誤

[英]SwiftUI App Crashing EXC_BAD_ACCESS error when coredata list items are deleted

以下代碼一直有效,直到對列表項執行刪除操作。 但這並不是每次代碼運行時都會發生的。 它因線程錯誤的EXC_BAD_ACCESS代碼而崩潰:

錯誤

然后我意識到,如果應用程序保持運行,則在刪除操作后一段時間后會發生此錯誤

這就是為什么這個錯誤很難弄清楚的原因。 但是當我將DispatchQueue.main.async方法添加到Model.swift中的tasks時,它開始出現。

此代碼的目的是當列表中發生任何更改時,使用self.fetchAll()方法從核心數據中重新加載更新的結果。

刪除紅線

我注意到的另一個問題是刪除后出現的紅線。

問題:

  1. 更新核心數據列表時如何用最少的代碼更新內容視圖結構? (此鏈接有不同的方法,每次在代碼中顯式調用 fetch 方法。)
  2. 如何用更少的代碼和更高的穩定性優化這段代碼?
  3. 圖片中的紅線問題是現有 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()
        }
    }
}

任務列表.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()
                }
            }
        }
    }
}

確保Task是可識別的:

extension Task: Identifiable {
}

如果你不知道你應該能夠在你的ForEach中做到這一點:

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

我其實也遇到過這個問題。 我必須做的解決方法是添加一個字段isDeleted而不是實際刪除 CoreData 中的記錄。 然后,您可以稍后在應用程序處於后台或終止時清理它。 或者只是將記錄保存在 Coredata 中。

由於我最近有類似的問題可以解決,我想我會試試你的例子。 我不確定您的問題是否仍然存在,因為該問題已經 5 個月大而沒有得到回答。

簡而言之:是的,您可以修復它。 但是您必須使用 SwiftUI 而不是反對它。 這意味着:您的 model 可以提供獲取請求,但您不應該自己執行獲取。 讓 SwiftUI 為您處理這個問題。

以下是所需的更改:

  1. AppDelegate.swift

創建 ContentView 時,您應該將托管的 object 上下文添加到 SwiftUI 環境中:

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

刪除您的tasks發布屬性和fetchAll function。 我們將讓 SwiftUI 處理這個問題。

  1. 內容視圖

刪除對model.fetchAll()的調用和對model.tasks.isEmpty的檢查,因為 ContentView 對任務一無所知。

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

使用這種方法確實對我很有效。 此外,刪除 animation 現在正在正常工作和繪圖。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM