I posted this question:
SwiftUI: deleting Managed Object from cell view crashes app?
as I worked on trying to understand why it crashes, I tried to change the model Item to have timestamp NON-optional:
extension Item {
@nonobjc public class func fetchRequest() -> NSFetchRequest<Item> {
return NSFetchRequest<Item>(entityName: "Item")
}
@NSManaged public var timestamp: Date
}
extension Item : Identifiable {
}
As Asperi pointed out, using this:
if let timestamp = item.timestamp {
Text(timestamp, formatter: itemFormatter)
}
does fix the crash when timestamp is optional.
However, this is just some code I am testing to understand how to properly build my views. I need to use models that do not have optional properties, and because of that I can't resort to use the provided answer to the question I linked to above.
So this question is to address the scenario where my CellView uses a property that is not optional on a ManagedObject.
If I were to put this code straight in the ContentView without using the CellView it does not crash. This does not crash:
struct ContentView: View {
@Environment(\.managedObjectContext) private var viewContext
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>
var body: some View {
NavigationView {
List {
ForEach(items) { item in
NavigationLink {
Text(item.timestamp, formatter: itemFormatter)
} label: {
// CellView(item: item)
HStack {
Text(item.timestamp, formatter: itemFormatter) // <<- CRASH ON DELETE
Button {
withAnimation {
viewContext.delete(item)
try? viewContext.save()
}
} label: {
Text("DELETE")
.foregroundColor(.red)
}
.buttonStyle(.borderless)
}
}
}
.onDelete(perform: deleteItems)
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
ToolbarItem {
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
}
Text("Select an item")
}
}
private func addItem() {
withAnimation {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
private func deleteItems(offsets: IndexSet) {
withAnimation {
offsets.map { items[$0] }.forEach { item in
viewContext.delete(item)
}
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
}
However, I need to know how to keep the CellView, use @ObservedObject and make this work. In this case is not a big deal to do that, but in real cases where the CellView is much bigger this approach does not scale well. Regardless, why would using @ObservedObject in a separate view be wrong anyway?
So, why is the app crashing when the timestamp is NOT optional in the model?
Why is the view trying to redraw the CellView for an Item that was deleted? How can this be fixed?
FOR CLARITY I AM POSTING HERE THE NEW CODE FOR THE NON-OPTIONAL CASE, SO YOU DON'T HAVE TO GO BACK AND LOOK AT THE LINKED QUESTION AND THEN CHANGE IT TO NON-OPTIONAL. THIS IS THE CODE THAT CRASHES IN ITS ENTIRETY:
struct ContentView: View {
@Environment(\.managedObjectContext) private var viewContext
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>
var body: some View {
NavigationView {
List {
ForEach(items) { item in
NavigationLink {
Text(item.timestamp, formatter: itemFormatter)
} label: {
CellView(item: item)
}
}
.onDelete(perform: deleteItems)
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
ToolbarItem {
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
}
Text("Select an item")
}
}
private func addItem() {
withAnimation {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
private func deleteItems(offsets: IndexSet) {
withAnimation {
offsets.map { items[$0] }.forEach { item in
viewContext.delete(item)
}
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
}
struct CellView: View {
@Environment(\.managedObjectContext) private var viewContext
@ObservedObject var item:Item
var body: some View {
HStack {
Text(item.timestamp, formatter: itemFormatter) // <<- CRASH ON DELETE
Button {
withAnimation {
viewContext.delete(item)
try? viewContext.save()
}
} label: {
Text("DELETE")
.foregroundColor(.red)
}
.buttonStyle(.borderless)
}
}
}
private let itemFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .medium
return formatter
}()
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}

The explicit handling is needed in anyway, because of specifics of CoreData engine. After object delete it can still be in memory (due to kept references), but it becomes in Fault state, that's why code autogeneration always set NSManagedObject properties to optional (even if they are not optional in model).
Here is a fix for this specific case. Tested with Xcode 13.4 / iOS 15.5
if !item.isFault {
Text(item.timestamp, formatter: itemFormatter) // << NO CRASH
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With