skip to Main Content

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

enter image description here

2

Answers


  1. 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
    }
    
    Login or Signup to reply.
  2. My 2 cents:

    The problem for me was that I tried to make as much as possible non optional in CoreData. This works for many cases but with Date it might lead to crashes. I declared my date property as optional again and everything worked without crashing.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search