'SwiftUI - How to close the sheet view, while dismissing that view

I want to achieve the function. Like, "Look up" view that is from Apple.

My aim is when the sheet view push another view by navigation, the user can tap the navigation item button to close the sheet view. Like, this below gif.

enter image description here

I try to achieve this function.

I found a problem that is when the user tap the "Done" button. The App doesn't close the sheet view. It only pop the view to parent view. Like, this below gif.

enter image description here

This is my code.

import SwiftUI

struct ContentView: View {
    @State var isShowSheet = false
    var body: some View {
        Button(action: {
            self.isShowSheet.toggle()
        }) {
            Text("Tap to show the sheet")
        }.sheet(isPresented: $isShowSheet) {
            NavView()
        }
    }
}

struct NavView: View {
    var body: some View {
        NavigationView {
            NavigationLink(destination: NavSubView()) {
                Text("Enter Sub View")
            }
        } .navigationViewStyle(StackNavigationViewStyle())
    }
}

struct NavSubView: View {
    @Environment(\.presentationMode) var presentationMode

    var body: some View {
        Text("Hello")
        .navigationBarItems(trailing:
            Button(action: {
                self.presentationMode.wrappedValue.dismiss()
            }){
                Text("Done")
            }
        )
    }
}

How did I achieve this function? :) Please help me, thank you. :)



Solution 1:[1]

UPDATE: Restored original version - provided below changes should be done, as intended, to the topic starter's code. Tested as worked with Xcode 13 / iOS 15

As navigation in sheet might be long enough and closing can be not in all navigation subviews, I prefer to use environment to have ability to specify closing feature only in needed places instead of passing binding via all navigation stack.

Here is possible approach (tested with Xcode 11.2 / iOS 13.2)

  1. Define environment key to store sheet state
struct ShowingSheetKey: EnvironmentKey {
    static let defaultValue: Binding<Bool>? = nil
}

extension EnvironmentValues {
    var showingSheet: Binding<Bool>? {
        get { self[ShowingSheetKey.self] }
        set { self[ShowingSheetKey.self] = newValue }
    }
}
  1. Set this environment value to root of sheet content, so it will be available in any subview when declared
}.sheet(isPresented: $isShowSheet) {
    NavView()
       .environment(\.showingSheet, self.$isShowSheet)
}
  1. Declare & use environment value only in subview where it is going to be used
struct NavSubView: View {
    @Environment(\.showingSheet) var showingSheet

    var body: some View {
        Text("Hello")
        .navigationBarItems(trailing:
            Button("Done") {
                self.showingSheet?.wrappedValue = false
            }
        )
    }
}

backup

Solution 2:[2]

I haven't tried SwiftUI ever, but I came from UIKit + RxSwift, so I kinda know how binding works. I read quite a bit of sample codes from a SwiftUI Tutorial, and the way you dismiss a modal is actually correct, but apparently not for a navigation stack.

One way I learned just now is use a @Binding var. This might not be the best solution, but it worked!

So you have this $isShowSheet in your ContentView. Pass that object to your NavView struct by declaring a variable in that NavView.

ContentView

    .....

    }.sheet(isPresented: $isShowSheet) {
        NavView(isShowSheet: self.$isShowSheet)
    }

NavView

struct NavView: View {
    @Binding var isShowSheet: Bool

    var body: some View {
        NavigationView {
            NavigationLink(destination: NavSubView(isShowSheet: self.$isShowSheet)) {
                Text("Enter Sub View")
            }
        } .navigationViewStyle(StackNavigationViewStyle())
    }
}

and finally, do the same thing to your subView.

NavSubView

struct NavSubView: View {
    @Environment(\.presentationMode) var presentationMode

    @Binding var isShowSheet: Bool

    var body: some View {
        Text("Hello")
        .navigationBarItems(trailing:
            Button(action: {
                //self.presentationMode.projectedValue.wrappedValue.dismiss()
                self.isShowSheet = false
            }){
                Text("Done")
            }
        )
    }
}

Now as you can see, you just need to send a new signal to that isShowSheet binding var - false.

self.isShowSheet = false

Voila!

enter image description here

Solution 3:[3]

Here's an improved version of Asperi's code from above since they won't accept my edit. Main credit goes to them.


As navigation in sheet might be long enough and closing can be not in all navigation subviews, I prefer to use environment to have ability to specify closing feature only in needed places instead of passing binding via all navigation stack.

Here is possible approach (tested with Xcode 13 / iOS 15)

  1. Define environment key to store sheet state

     struct ShowingSheetKey: EnvironmentKey {
         static let defaultValue: Binding<Bool>? = nil
     }
    
     extension EnvironmentValues {
         var isShowingSheet: Binding<Bool>? {
             get { self[ShowingSheetKey.self] }
             set { self[ShowingSheetKey.self] = newValue }
         }
     }
    
  2. Set this environment value to root of sheet content, so it will be available in any subview when declared

     @State var isShowingSheet = false
    
     ...
    
     Button("open sheet") {
         isShowingSheet?.wrappedValue = true
     }
     // note no $ in front of isShowingSheet
     .sheet(isPresented: isShowingSheet ?? .constant(false)) {
         NavView()
            .environment(\.isShowingSheet, self.$isShowingSheet)
     }
    
  3. Declare & use environment value only in subview where it is going to be used

     struct NavSubView: View {
         @Environment(\.isShowingSheet) var isShowingSheet
    
         var body: some View {
             Text("Hello")
             .navigationBarItems(trailing:
                 Button("Done") {
                     isShowingSheet?.wrappedValue = false
                 }
             )
         }
     }
    

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Glenn Posadas
Solution 3 LinusGeffarth