How should my code be modified to ensure that when an exception happens at the Core Data layer when adding a new item, that SwiftUI does NOT continue to show a new item?
Background: When I run the below code I get an exception when adding a new item doing the "context.save()", HOWEVER whilst the new item request really failed (did not save to Core Data), the UI does show a new item. It is as if the "lists" variable in the @FetchRequest line is not behaving dynamically.
Question - How do I fix code so that the application works properly?
Code:
import SwiftUI
struct ContentView: View {
@Environment(\.managedObjectContext) var context
@FetchRequest(fetchRequest: List.allListFetchRequest()) var lists: FetchedResults
private func addListItem() {
let newList = List(context: context)
newList.id = 1
newList.title = "Testing 123"
do {
try context.save()
} catch let e as NSError {
print("Could not save new List. \(e.debugDescription)")
return
}
}
var body: some View {
NavigationView {
VStack {
ForEach(lists) { list in
Text("List = \(list.title)")
}
}
.navigationBarTitle( Text("My Todo Lists") )
.navigationBarItems(
trailing: Button(action: {self.addListItem()} ) {
Image(systemName: "plus.square")
}
)
}
}
}
Example Output:
Could not save new List. Error Domain=NSCocoaErrorDomain Code=133021 "(null)" UserInfo={NSExceptionOmitCallstacks=true, conflictList=(
"NSConstraintConflict (0x600001fcccc0) for constraint (\n id\n): database: (null), conflictedObjects: (\n \"0x600000a7e360 \",\n \"0xfb1bb528bb57810c \"\n)"
)}
This is happening, I believe, because the content of the in-memory object graph and the on-disk storage are different things, and SwiftUI is watching one, while the error is talking about the other.
SwiftUI is looking at the contents of your ManagedObjectContext. Since you've created a new item, it sees that (you had 2 items, now you have 3). When you try to save, that operation fails, and it gives you information on why—in this case, the `id` property value isn't unique—so that you can modify the version in your context and try again. The object is still there, though, so SwiftUI still sees it and displays it.
So, to prevent SwiftUI showing an object that couldn't be saved, the solution is actually fairly straightforward: in your `catch` block, delete the offending object from the ManagedObjectContext:
private func addListItem() {
context.performBlock { context in
let newList = List(context: context)
newList.id = 1
newList.title = "Testing 123"
do {
try context.save()
}
catch {
print("Failed to save new item. Error = \(error)")
context.delete(newList)
// don't need to save here, because we're in `performBlock` and have reverted the only unsaved change.
}
}
}
Also, it's a good idea to use `performBlock` when working with your managed object contexts, because threading issues with CoreData are really insiduous and quite hard to debug.