16

I am using .sheet(isPresented: self.$showModal) in my root view to present a modal. Within the modal, I am using NavigationView to take the user through various pages (for a user profile builder).

In the last page of my navigation stack, I am using @Environment(\.presentationMode) var presentationMode and a button which calls self.presentationMode.wrappedValue.dismiss() to dismiss the modal. However, this only dismisses the last page in the navigation stack, and I just end up at the previous page. What I want to do is dismiss the entire navigation stack and end up back at the root view.

Swiping down to dismiss the modal produces the desired result, but I want to do this programatically with the button.

Is this currently possible in SwiftUI, or is this a problem with using NavigationView within a modal sheet?

gotnull
  • 26,454
  • 22
  • 137
  • 203
lmh
  • 293
  • 1
  • 4
  • 13
  • In the past simply calling `self.view.window!.rootViewController?.dismiss(animated: false, completion: nil)` would dismiss all view controllers above the root view controller. – gotnull Oct 09 '19 at 05:42
  • Does this answer your question? [Multiple sheet(isPresented:) doesn't work in SwiftUI](https://stackoverflow.com/questions/58837007/multiple-sheetispresented-doesnt-work-in-swiftui) – Curiosity Mar 02 '21 at 23:06

5 Answers5

22

Solution 1 - Custom EnvironmentKey

A possible solution is to use a custom EnvironmentKey injected to every environment:

struct PresentationKey: EnvironmentKey {
    static let defaultValue: [Binding<Bool>] = []
}

extension EnvironmentValues {
    var presentations: [Binding<Bool>] {
        get { return self[PresentationKey] }
        set { self[PresentationKey] = newValue }
    }
}

Demo:

enter image description here

struct ContentView: View {
    @Environment(\.presentations) private var presentations
    @State private var showSheet = false

    var body: some View {
        Button("Show sheet") {
            showSheet = true
        }
        .sheet(isPresented: $showSheet) {
            SheetView()
                .environment(\.presentations, presentations + [$showSheet])
        }
    }
}
struct SheetView: View {
    @Environment(\.presentations) private var presentations
    @State private var showSheet = false

    var body: some View {
        Button("Show another sheet") {
            showSheet = true
        }
        .sheet(isPresented: $showSheet) {
            SheetNavigationView()
                .environment(\.presentations, presentations + [$showSheet])
        }
    }
}
struct SheetNavigationView: View {
    var body: some View {
        NavigationView {
            NavigationLink("Link", destination: SheetNavigationDetailView())
        }
    }
}
struct SheetNavigationDetailView: View {
    @Environment(\.presentations) private var presentations

    var body: some View {
        Button("Pop to root") {
            presentations.forEach {
                $0.wrappedValue = false
            }
        }
    }
}

Solution 2 - Dismiss UIKit rootViewController

struct SheetNavigationDetailView: View {

    var body: some View {
        Button("Pop to root") {
            UIApplication.shared.windows.first?.rootViewController?.dismiss(animated: true)
        }
    }
}
Edward Brey
  • 40,302
  • 20
  • 199
  • 253
pawello2222
  • 46,897
  • 22
  • 145
  • 209
4

You can pass showModal as a binding into the following screens and instead of using presentationValue set showModal to false.

LuLuGaGa
  • 13,089
  • 6
  • 49
  • 57
  • Thanks, this worked for me. I ended up making an @Binding var showModal in each page of my NavigationStack, then passed the binding into each successive page until I got to the end where I actually wanted to toggle it. Not sure if that was the most efficient way to do it, but it works. – lmh Oct 10 '19 at 01:27
  • 2
    Is there any chance you could share a full sample of this? I am greatly struggling with this same approach. Passing `showModal` as a binding works, but each item in my navigation stack is just added slightly below the last one, not actually replacing it. – ZbadhabitZ Apr 03 '20 at 18:21
3
@State private var modalOpen: Bool = false

.sheet(isPresented: self.$modalOpen, onDismiss: {
            // code that triggers when modal is closed
        }) {
        Button(action: {
            self.modalOpen.toggle()
        }) {
           Text("Schließen")
        }
seref
  • 543
  • 2
  • 6
  • 19
-1

In SwiftUI.

Use SheetKit to dismiss all sheets with animation controll.

SheetKit().dismissAllSheets(animation:false)

or Use NavigationViewKit to pop back to root view in navigationView

Bob Xu
  • 129
  • 2
  • 9
-1

Here is the full functional example of nested modals closed on the last screen ( number #3 ) from the Close button for someone who might need it.

So below we have the main screen where we initiate and trigger modal:

struct OnboardingFlowModal: View {
    @State private var showOnboardingFlow: Bool = false
    
    var body: some View {

        VStack(spacing: 50){
            Text("content of main screen".capitalized)
            
            Spacer()
            
            Button {
                showOnboardingFlow.toggle()
            } label: {
                Text("Start Onboarding")
            }
            .buttonStyle(.borderedProminent)
            .buttonBorderShape(.roundedRectangle(radius: 12))
            .controlSize(.large)
            .padding()
            
        }
        .sheet(isPresented: $showOnboardingFlow,
               onDismiss: didDismiss) {
            ScreenOne(showOnboardingFlow: $showOnboardingFlow)
        }
        
    }
    
    func didDismiss(){
        //dismiss function here
        
    }
}

Screen #1, which starts the navigation flow for the modal screens:

struct ScreenOne: View {
    
    @Binding var showOnboardingFlow: Bool
    
    var body: some View{
        NavigationView {
            VStack{
                Text("Welcome to screen #1".capitalized)
            }
            .navigationTitle("Screen #1")
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    NavigationLink {
                        ScreenTwo(showOnboardingFlow: $showOnboardingFlow)
                    } label: {
                        Text("Next")
                    }
                    
                }
            }
        }
    }
}

Screen #2 for illustrative purposes, where we have the button towards the last modal in the navigation:

struct ScreenTwo: View {
    
    @Binding var showOnboardingFlow: Bool
    
    var body: some View{
        
        VStack{
            Text("Welcome to screen #2".capitalized)
        }
        .navigationTitle("Screen #2")
        .toolbar {
            ToolbarItem(placement: .navigationBarTrailing) {
                NavigationLink {
                    ScreenThree(showOnboardingFlow: $showOnboardingFlow)
                } label: {
                    Text("Next")
                }
                
            }
        }
    }
}

Screen #3, where the Close button is displayed and it closes all of the nested modals:

struct ScreenThree: View {
    
    @Binding var showOnboardingFlow: Bool
    
    var body: some View{
        VStack{
            Text("Welcome to screen #3".capitalized)
        }
        .navigationTitle("Screen #3")
        .toolbar {
            ToolbarItem(placement: .navigationBarTrailing) {
                Button {
                    showOnboardingFlow = false
                } label: {
                    Text("Close")
                }
            }
        }
    }
}
Bobby
  • 1
  • 1
  • As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Aug 30 '23 at 18:14