Reputation: 954
The following code is working all the way until the delete operation is done on list items. But this is not occurring every time the code is running. It crashes with EXC_BAD_ACCESS
code of thread 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.
Another problem I noticed is the red lines appearing after deleting.
Questions:
macOS: 10.15.4
XCode: 11.5
Target: 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()
}
}
}
}
}
Upvotes: 0
Views: 2061
Reputation: 2642
As I had similar problems I could fix recently, I thought I will give your example a try. I'm not sure if your problem persists, as the question is already 5 months old without being answered.
In short: Yes, you can fix it. But you will have to work with SwiftUI and not against it. This means: Your model can provide the fetch request but you should not execute the fetch yourself. Let SwiftUI handle this for you.
Here are the required changes:
You should add your managed object context into the SwiftUI environment when creating ContentView:
let contentView = ContentView()
.environmentObject(model)
.environment(\.managedObjectContext, persistentContainer.viewContext)
Remove your tasks
published property and the fetchAll
function. We will let SwiftUI handle this.
Remove the call to model.fetchAll()
and the check for the model.tasks.isEmpty
, as the ContentView doesn't know anything about the tasks.
Add FetchRequest
to create your tasks
property and add here the check for the tasks.isEmpty
. Here is the code:
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()
}
}
Using this approach did work for me nicely. Also the delete animation is now working and drawing as it should.
Upvotes: 0
Reputation: 168
Make sure Task
is an Identifiable:
extension Task: Identifiable {
}
If you dont know you should be able to do this in your ForEach
:
ForEach(self.model.tasks) {task in
}
I've actually had this issue as well. The workaround I had to do was to add a field isDeleted
instead of actually deleting the record off CoreData. Then you can either clean it up later, when app is on background or terminated. or just keep the record in Coredata.
Upvotes: 0