SwiftUI sheet not dismissing when isPresented value changes from a closure

Viewed 1665

I have a sheet view that is presented when a user clicks a button as shown in the parent view below:

struct ViewWithSheet: View {
    @State var showingSheetView: Bool = false
    @EnvironmetObject var store: DataStore()

    var body: some View {
        NavigationView() {
            ZStack {
               Button(action: { self.showingSheetView = true }) {
                   Text("Show sheet view")
               }
            }
            .navigationBarHidden(true)
            .navigationBarTitle("")
            .sheet(isPresented: $showingSheetView) {
                SheetView(showingSheetView: self.$showingSheetView).environmentObject(self.dataStore)
            }
        }
    }
}

In the sheet view, when a user clicks another button, an action is performed by the store that has a completion handler. The completion handler returns an object value, and if that value exists, should dismiss the SheetView.

struct SheetView: View {

    @Binding var showingSheetView: Bool

    @EnvironmentObject var store: DataStore()

    //@Environment(\.presentationMode) private var presentationMode

    func create() {
        store.createObject() { object, error in 
           if let _ = object {
               self.showingSheetView = false
               // self.presentationMode.wrappedValue.dismiss()
           }
        }
    }

    var body: some View {
        VStack {
            VStack {
                HStack {
                    Button(action: { self.showingSheetView = false }) {
                        Text("Cancel")
                    }
                    
                    Spacer()
                    
                    Spacer()
                    
                    Button(action: { self.create() }) {
                        Text("Add")
                    }
                }
                .padding()
            }
        }
    }
}

However, in the create() function, once the store returns values and showingSheetView is set to false, the sheet view doesn't dismiss as expected. I've tried using presentationMode to dismiss the sheet as well, but this also doesn't appear to work.

3 Answers

I found my issue, the sheet wasn't dismissing due to a conditional in my overall App wrapping View, I had an if statement that would show a loading view on app startup, however, in my DataStore I was setting it's fetching variable on every function call it performs. When that value changed, the view stack behind my sheet view would re-render the LoadingView and then my TabView once the fetching variable changed again. This was making the sheet view un-dismissable. Here's an example of what my AppView looked like:

struct AppView: View {

    @State private var fetchMessage: String = ""

    @EnvironmentObject var store: DataStore()

    func initializeApp() {
        self.fetchMessage = "Getting App Data"
        store.getData() { object, error in 
            if let error = error {
                self.fetchMessage = error.localizedDescription
            }

            self.fetchMessage = ""
        }
    }

    var body: some View {
        Group {
            ZStack {
                //this is where my issue was occurring
                if(!store.fetching) {
                    TabView {
                        Tab1().tabItem {
                            Image(systemName: "tab-1")
                            Text("Tab1")
                        }

                        Tab2().tabItem {
                            Image(systemName: "tab-2")
                            Text("Tab2")
                        }

                        //Tab 3 contained my ViewWithSheet() and SheetView()
                        Tab3().tabItem {
                            Image(systemName: "tab-3")
                            Text("Tab3")
                        }
                    }
                } else {
                    LoadingView(loadingMessage: $fetchMessage)
                }
            }
        }.onAppear(perform: initializeApp)
    }
}

To solve my issue, I added another variable to my DataStore called initializing, which I use to render the loading screen or the actual application views on first .onAppear event in my app. Below is an example of what my updated AppView looks like:

struct AppView: View {

    @State private var fetchMessage: String = ""

    @EnvironmentObject var store: DataStore()

    func initializeApp() {
        self.fetchMessage = "Getting App Data"
        store.getData() { object, error in 
            if let error = error {
                self.fetchMessage = error.localizedDescription
            }

            self.fetchMessage = ""
            //set the value to false once I'm done getting my app's initial data.
            self.store.initializing = false
        }
    }

    var body: some View {
        Group {
            ZStack {
                //now using initializing instead
                if(!store.initializing) {
                    TabView {
                        Tab1().tabItem {
                            Image(systemName: "tab-1")
                            Text("Tab1")
                        }

                        Tab2().tabItem {
                            Image(systemName: "tab-2")
                            Text("Tab2")
                        }

                        //Tab 3 contained my ViewWithSheet() and SheetView()
                        Tab3().tabItem {
                            Image(systemName: "tab-3")
                            Text("Tab3")
                        }
                    }
                } else {
                    LoadingView(loadingMessage: $fetchMessage)
                }
            }
        }.onAppear(perform: initializeApp)
    }
}

Try to do this on main queue explicitly

func create() {
    store.createObject() { object, error in 
       if let _ = object {
           DispatchQueue.main.async {
              self.showingSheetView = false
           }
       }
       // think also about feedback on else case as well !!
    }
}

Want to see something hacky that worked for me? Disclaimer: Might not work for you and I don't necessarily recommend it. But maybe it'll help someone in a pinch.

If you add a NavigationLink AND keep your fullScreenCover, then the fullscreen cover will be able to dismiss itself like you expect.

Why does this happen when you add the NavigationLink to your View? I don't know. My guess is it creates an extra reference somewhere.

Add this to your body, and keep your sheet as it is:

NavigationLink(destination: YOURVIEW().environmentObjects(), isActive: $showingSheetView) {}

Related