SwiftUI TabView with List not refreshing after objected deleted from Core Data

Description:

When an object in a list (created from a fetchrequest) is deleted from a context, and the context is saved, the list does not properly update.


Error:

Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value (Thrown on line 5 below)

struct DetailView: View {
    @ObservedObject var event: Event
    
    var body: some View {
        Text("\(event.timestamp!, formatter: dateFormatter)")
            .navigationBarTitle(Text("Detail"))
    }
}


Steps to reproduce:

1. Create a new Master Detail App project with SwiftUI and Core Data.

2. In the ContentView, set the body to a TabView with the first tab being the prebuilt NavigationView, and add a second arbitrary tab.

struct ContentView: View {
    @Environment(\.managedObjectContext)
    var viewContext   
    
    var body: some View {
        TabView {
            NavigationView {
                MasterView()
                    .navigationBarTitle(Text("Master"))
                    .navigationBarItems(
                        leading: EditButton(),
                        trailing: Button(
                            action: {
                                withAnimation { Event.create(in: self.viewContext) }
                        }
                        ) {
                            Image(systemName: "plus")
                        }
                )
                Text("Detail view content goes here")
                    .navigationBarTitle(Text("Detail"))
            }
            .navigationViewStyle(DoubleColumnNavigationViewStyle())
            .tabItem { Text("Main") }
            
            Text("Other Tab")
                .tabItem { Text("Other Tab") }
        }
    }
}

3. Add a few items. Interact with those items in any way.

4. Change tabs.

5. Change back to Main Tab.

6. Attempt to delete an item.


Notes:

  • The events: FetchResults<Event> appears to be updated properly following the deletion of the object. However, when the List is rerendered, an error occurs when trying to access the event's timestamp, because it wasn't removed from the list.
  • The address of the managedObjectContext variable is the same in both the ContentView and the MasterView. This is known to be a problem when accesing the managedObjectContext from a modal view, as it must by passed again to ensure it is using the same context. This, however, does not seem to be an issue in this case.


Details:

XCode Version 11.3 (11C29)

Replies

For what it's worth, it's not the List causing the crash, it's the Detail view. When you switch tabs back to the one containing the double-pane navigation view, the onscreen Detail view is going to re-render. The one that was there has a reference to what is now a fault, and it will yield a nil value for any attributes since the underlying data has been removed.


The ultimate reason for the crash is the implicitly unwrapped optional in your DetailView. You can solve it at that level by using a default presentation of some kind:


var body: some View {
    if event.managedObjectContext == nil {
        // object has been deleted
        return Text("No Selection")
            .navigationBarTitle("Detail")
    }

    return Text("\(event.timestamp ?? .distantPast, formatter: dateFormatter)")
        .navigationBarTitle("Detail")
}


It seems that the issue revolves around the fact that the saved view hierarchy includes a DetailView instance that contains that dangling object. I'll see if I can figure out how to force that item's removal when the List is offscreen—it's the 'offscreen' part that's throwing a wrench into the works here.

You are correct, but I believe there is more to the story. That solves the error that was initially raised, but doesn't actually adequately refresh the Master view with an update FetchRequest. Upon inserting the change to Detail view you suggested, the error no longer occurs. When trying to delete an item from the list though, the view still shows the item. It doesn't throw an error, but the item is not removed from the list. Again below are the only changes I have made to the sample project.


struct ContentView: View {
    @Environment(\.managedObjectContext)
    var viewContext   
    
    var body: some View {
        TabView {
            NavigationView {
                MasterView()
                    .navigationBarTitle(Text("Master"))
                    .navigationBarItems(
                        leading: EditButton(),
                        trailing: Button(
                            action: {
                                withAnimation { Event.create(in: self.viewContext) }
                        }
                        ) {
                            Image(systemName: "plus")
                        }
                )
                Text("Detail view content goes here")
                    .navigationBarTitle(Text("Detail"))
            }
            .navigationViewStyle(DoubleColumnNavigationViewStyle())
            .tabItem{ Text("Main") }
            
            Text("Other Tab")
                .tabItem{ Text("Other Tab") }
        }
        .onAppear {
            print(self.viewContext)
        }
    }
}


struct DetailView: View {
    @ObservedObject var event: Event
    
    var body: some View {
        Text("\(event.timestamp ?? .distantPast, formatter: dateFormatter)")
            .navigationBarTitle(Text("Detail"))
    }
}


For clarification, when first navigating to the Main tab, the view works as expected: items, when deleted, are promptly removed form the list with the standard animation and the view is refreshed. After navigating to another tab, and then navigating back to the Main tab, this is not the case. After returning to the main tab, deleting an item from the list does not actually delete it from the list in the view. The deletion is successful from core data (evident by the fact that switching again to the other tab and back again causes the item to no appear), but the view is not immediately refreshed, which is the expected and previosly observed effect.

I was having the exact same problem, and I assume it's a bug with SwiftUI. My project's on iOS so I found that using a UIKit implementationfixed this problem. I found the code from this SO answer where they talk about TabView resetting the navigation stack when tabs are switched. Hope this helps.

I found a pure SwiftUI working solution:

/// This will init and deinit the content view when selection math tag.

struct SyncView<Content: View>: View {

@Binding var selection: Int

var tag: Int

var content: () -> Content

@ViewBuilder

var body: some View {

if selection == tag {

content()

} else {

Spacer()

}

}

}

You can use it then in this way:

struct ContentView: View {
    @State private var selection = 0

    var body: some View {
        TabView(selection: $selection) {
            
            SyncView(selection: $selection, tag: 0) {
                ViewThatNeedsRefresh()
            }
            .tabItem { Text("First") }
            .tag(0)
            
            Text("Second View")
                .font(.title)
                .tabItem { Text("Second") }
                .tag(1)
        }
    }
}

You can use the SyncView for each view that needs a refresh.