Сбой приложения SwiftUI Ошибка EXC_BAD_ACCESS при удалении элементов списка общих данных - PullRequest
0 голосов
/ 30 мая 2020

Следующий код работает до тех пор, пока операция удаления не будет выполнена для элементов списка. Но это происходит не каждый раз при запуске кода. Он вылетает с кодом ошибки потока EXC_BAD_ACCESS:

ERROR

Then I realized, this error is occurring after a certain time after delete operation if the app is kept running

This is why this error is very confusing to figure out. But it started appearing when I added DispatchQueue.main.async method to tasks in Model.swift.

The purpose of this code is to reload updated results from core data using self.fetchAll() method when there is any change happened in the list.

Red Line on Delete

Another problem I noticed is the red lines appearing after deleting.

Questions:

  1. How to update content view struct with least code when a core data list is updated? (This ссылка имеет другой подход, поскольку метод выборки вызывается каждый раз явно в коде.)
  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()
        }
    }
}

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

1 Ответ

0 голосов
/ 31 мая 2020

Убедитесь, что Task является идентифицируемым:

extension Task: Identifiable {
}

Если вы не знаете, вы сможете сделать это в своем ForEach:

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

Я на самом деле тоже была эта проблема. Обходной путь, который мне пришлось сделать, - это добавить поле isDeleted вместо фактического удаления записи из CoreData. Затем вы можете очистить его позже, когда приложение находится в фоновом режиме или завершено. или просто ведите учет в Coredata.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...