0

I have a CoreData entity called TestItem:

enter image description here

If there's 1 entity in collection, deleting this entity will crash the app with an error: "Thread 1: EXC_BREAKPOINT (code=1, subcode=0x197c0b8b4)". As it seems, nothing will prevent DetailView from crashing once it's been initialized with ObservedObject, which cannot be nil. I can't find a way to deinit DetailView before deleting the object. To have more information why I couldn't deinit DetailView, see my previous post

Here is a code example reproducing the error as following:


import SwiftUI
import CoreData

struct ContentView: View {
    let persistence = PersistenceController.shared
    
    @FetchRequest(entity: TestItem.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \TestItem.date, ascending: false)])
    var items: FetchedResults<TestItem>
    
    @State var selectedItem: TestItem?
    @State var selectedItemIndex: Int?
    
    var body: some View {
        NavigationSplitView {
            List(selection: $selectedItem) {
                ForEach(items) { item in
                    VStack(alignment: .leading) {
                        Text(item.name)
                            .foregroundColor(selectedItem == item ? .blue : .white)
                    }
                    .onTapGesture {
                        selectedItem = item
                        selectedItemIndex = items.firstIndex(of: item)
                    }
                }
            }
            .toolbar {
                Button(action: {
                    addItem()
                }) {
                    Image(systemName: "plus.square")
                }
            }
        } detail: {
            if let selectedItem = selectedItem {
                DetailView(item: selectedItem, onDelete: {
                    guard !items.isEmpty else {
                        self.selectedItem = nil
                        selectedItemIndex = nil
                        return
                    }
                    
                    if selectedItemIndex! > items.indices.last! {
                        selectedItemIndex = selectedItemIndex! - 1
                    }
                    
                    self.selectedItem = items[selectedItemIndex!]
                })
            }
        }
    }
    
    private func addItem() {
        let newItem = TestItem(context: persistence.container.viewContext)
        newItem.name = "Test Item"
        newItem.date = Date()
        
        do {
            try persistence.container.viewContext.save()
            selectedItem = newItem
            selectedItemIndex = 0
        } catch {
            let nsError = error as NSError
            fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
        }
    }
}

struct DetailView: View {
    @ObservedObject var item: TestItem
    var onDelete: () -> Void
    
    var body: some View {
        VStack {
            Text(item.name)
            
            DatePicker("Date", selection: $item.date, displayedComponents: [.date])
            
            HStack {
                Button(action: {
                    deleteItem(item)
                }) {
                    Image(systemName: "trash")
                }
                Spacer()
            }
            
            Spacer()
        }
    }
    
    private func deleteItem(_ item: TestItem) {
        item.managedObjectContext?.delete(item)

        DispatchQueue.main.async {
            do {
                try item.managedObjectContext?.save()
                onDelete()
            } catch let error as NSError {
                print("Error deleting data: \(error.localizedDescription)")
            }
        }
    }
}

netsplatter
  • 559
  • 3
  • 14

1 Answers1

1

It turns out that the crashed is caused by the DatePicker that I guess holds a strong reference to the Item object. If we break this reference the app will not crash when the last object is deleted.

This can be done by using a State object instead for the DatePicker selection

@State private var selectedDate: Date = .now

used here

DatePicker("Date", selection: $selectedDate, displayedComponents: [.date])

and update the item when this property is changed

.onChange(of: selectedDate) { date in
    item.date = date
}

We also need to update selectedDate when a new TestItem object is selected in the sidebar

.onChange(of: item) { newItem in
    selectedDate = newItem.date
}

I don't know if this relevant for any other components than the DatePicker, I for instance changed so that I could edit the name attribute using a TextField but that didn't cause any issues so it's definitely not an issue for all components where you can mutate the content.

Joakim Danielson
  • 43,251
  • 5
  • 22
  • 52
  • Unfortunately, this approach will create another problem with detecting changes and transferring selectedDate state to item. If I use .onChange detecting change of item.objectID to update selectedDate, I will not be able to detect changes in selectedDate without unwanted behavior when first .onChange will trigger the second .onChange and will overwrite item.date with previous selectedDate value. – netsplatter May 07 '23 at 03:55
  • Yes I also had some issues with date but I thought it was beyond the question to solve that as well. Maybe I can take a look at it again. – Joakim Danielson May 07 '23 at 04:56
  • 1
    A simple `onChange(of:)` fixed it. – Joakim Danielson May 07 '23 at 13:16
  • Thank you. And I will have to use a flag to prevent triggering .onChange(of: selectedDate) right after .onChange(of: item) changed selectedDate. – netsplatter May 08 '23 at 03:00