删除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种实现