2
votes

I am trying to create a very basic app with two views. The main view displays CoreData objects in a NavigationView and has a toolbar button to open an "add item" modal sheet. The "add item" sheet should be able to insert a new CoreData object and dismiss itself. What I am observing is that the modal sheet can be dismissed as long as it doesn't insert a CoreData object, but once it does interact with CoreData, all my attempts to dismiss the sheet no longer work. Also, when the sheet creates a CoreData object, I get the following error:

[Presentation] Attempt to present <TtGC7SwiftUI22SheetHostingControllerVS_7AnyView: 0x7f8fcbc49a10> on <TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier_: 0x7f8fcbd072c0> (from <TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVVS_22_VariadicView_Children7ElementGVS_18StyleContextWriterVS_19SidebarStyleContext__: 0x7f8fbbd0ba80>) which is already presenting <TtGC7SwiftUI22SheetHostingControllerVS_7AnyView: 0x7f8fcbd310b0>.

Most of the code I am using is Xcode's own boilerplate code, plus common patterns borrowed from the web. Can you please help me to debug?

To reproduce, in Xcode 12, create a new SwiftUI iOS app using CoreData. Replace the ContentView.swift with this:

import SwiftUI
import CoreData

struct ContentView: View {
    @Environment(\.managedObjectContext) private var viewContext

    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
        animation: .default)
    private var items: FetchedResults<Item>
    
    @State var showingSheet: Bool = false

    var body: some View {
        NavigationView {
            List {
                //Text("static text")

                ForEach(items) { item in
                    Text("Item at \(item.timestamp!, formatter: itemFormatter)")
                }
                .onDelete(perform: deleteItems)
            }
            .navigationTitle("List of items")
            .toolbar {
                ToolbarItem(placement: .primaryAction) {
                    Button(action: {
                        showingSheet = true
                    }) {
                        Text("Open sheet")
                    }
                    .sheet(isPresented: $showingSheet) {
                        MySheetView(isPresented: $showingSheet)
                            .environment(\.managedObjectContext, viewContext)
                    }
                }
            }
        }
    }

    private func deleteItems(offsets: IndexSet) {
        withAnimation {
            offsets.map { items[$0] }.forEach(viewContext.delete)
            do {
                try viewContext.save()
            } catch {
                let nsError = error as NSError
                fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
            }
        }
    }
}

private let itemFormatter: DateFormatter = {
    let formatter = DateFormatter()
    formatter.dateStyle = .short
    formatter.timeStyle = .medium
    return formatter
}()

Create a new MySheetView.swift:

struct MySheetView: View {
    @Environment(\.managedObjectContext) private var viewContext
    @Binding var isPresented: Bool
    
    var body: some View {
        NavigationView {
            Form {
                Button(action: {
                    self.isPresented = false
                }) {
                    Text("Dismiss this sheet")
                }
                
                Button(action: {
                    let newItem = Item(context: viewContext)
                    newItem.timestamp = Date()

                    do {
                        try viewContext.save()
                    } catch {
                        let nsError = error as NSError
                        fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
                    }
                }) {
                    Text("Add Item")
                }
            }
        }
    }
}

When running, if you tap "Open sheet", you can then tap "Dismiss this sheet" and it works. But if you tap "Open sheet" and then tap "Add Item", the error is printed and the "Dismiss this sheet" button stops working. (You can still swipe down to dismiss the sheet.)

I have also tried the presentationmode.wrappedvalue.dismiss() method with no success.

I have found that if you comment out the ForEach loop on the main view and uncomment the Text("static text"), then adding items on the sheet no longer prints out the error or breaks dismissing of the sheet. So the problem somehow is related to presenting the data on the main sheet. But obviously I do need to actually display the data, and it seems like a really common and basic pattern. Am I doing it wrong?

1

1 Answers

10
votes

Your .sheet is placed on the ToolbarItem. Just change it on the NavigationView and it works.

Here is the body of the ContentView

var body: some View {
    NavigationView {
        List {
            //Text("static text")

            ForEach(items, id:\.self) { item in
                Text("Item at \(item.timestamp!, formatter: itemFormatter)")
            }
            .onDelete(perform: deleteItems)
        }
        .navigationTitle("List of items")
        .toolbar {
            ToolbarItem(placement: .primaryAction) {
                Button(action: {
                    showingSheet = true
                }) {
                    Text("Open sheet")
                }
            }
        }
        .sheet(isPresented: $showingSheet) {
            MySheetView(isPresented: $showingSheet)
                .environment(\.managedObjectContext, viewContext)
        }

    }
}