154

I couldn't find any reference about any ways to make a pop or a dismiss programmatically of my presented view with SwiftUI.

Seems to me that the only way is to use the already integrated slide dow action for the modal(and what/how if I want to disable this feature?), and the back button for the navigation stack.

Does anyone know a solution? Do you know if this is a bug or it will stays like this?

Andrea Miotto
  • 7,084
  • 8
  • 45
  • 70
  • Given the current API status, you will have to implement those transitions yourself. – Matteo Pacini Jun 09 '19 at 10:05
  • You can now do this in Beta 5 for both Navigation and Modals. See my answer below. – Chuck H Jul 30 '19 at 20:32
  • Take a look at this open source project: https://github.com/biobeats/swiftui-navigation-stack It's an alternative navigation stack for SwiftUI and, among other things, it offers the possibility to push/pop programmatically. It would be great if you guys joined me in improving this project. – superpuccio Feb 04 '20 at 15:19
  • @Andrea, you were able to solve it? Im still stuck over here – mohsin Jul 07 '20 at 16:54
  • Here you can find the simplest answer with example :
    https://stackoverflow.com/a/62863487/12534983
    – Sapar Friday Jul 12 '20 at 16:25
  • The best answer now, is to use navigationLink `tag` and `selection` with an environmentobject that tracks the selection. You can then set the environmentobject's `selection` to `nil` to go back to root view from any child view. – Super Noob Sep 07 '20 at 05:48
  • Starting from iOS 15 we can use [DismissAction](https://developer.apple.com/documentation/swiftui/dismissaction?changes=latest_minor) - see [this answer](https://stackoverflow.com/a/67994346/8697793). – pawello2222 Jun 15 '21 at 22:50
  • Checkout SwiftUI navigation library github.com/canopas/UIPilot for easy navigation. It does not replace NavigationView but it wraps `NavigationView` in a way that's really easy to use. – jimmy0251 Feb 24 '22 at 10:37

11 Answers11

185

This example uses the new environment var documented in the Beta 5 Release Notes, which was using a value property. It was changed in a later beta to use a wrappedValue property. This example is now current for the GM version. This exact same concept works to dismiss Modal views presented with the .sheet modifier.

import SwiftUI

struct DetailView: View {
    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
    var body: some View {
        Button(
            "Here is Detail View. Tap to go back.",
            action: { self.presentationMode.wrappedValue.dismiss() }
        )
    }
}

struct RootView: View {
    var body: some View {
        VStack {
            NavigationLink(destination: DetailView())
            { Text("I am Root. Tap for Detail View.") }
        }
    }
}

struct ContentView: View {
    var body: some View {
        NavigationView {
            RootView()
        }
    }
}
Chuck H
  • 7,434
  • 4
  • 31
  • 34
  • 2
    This is really good! I just wish it worked for the doubleColumn navigation too to let us see the sidebar of the split view, for times when the user starts an iPad in portrait mode. – MScottWaller Aug 08 '19 at 20:14
  • 1
    I think this should be the accepted answer. It's very clean and doesn't need any modification on the parent view. – Maetthu24 Oct 16 '19 at 07:20
  • 2
    This is a great iOS solution, which I know was the main aim of the OP. Sadly though, it doesn't appear to work for macOS navigation lists, where both the list & view are shown simultaneously. Any known approach for that? – TheNeil Dec 16 '19 at 17:05
  • How to call the RootView from button? – J A S K I E R May 04 '20 at 00:42
  • I think this is what you want: https://stackoverflow.com/questions/57334455/swiftui-how-to-pop-to-root-view/59662275#59662275 – Chuck H May 04 '20 at 17:48
  • @mohsin - What exactly do you mean by it's not working? Try creating a new SwiftUI iOS project and replace the ContentView struct with all of the code from my answer above. It still seems to work just fine. – Chuck H Jul 07 '20 at 18:51
  • your code is working fine but when I tried to do the same in my project. It's not working and also it doesn't give any error. Im using xcode 11.5 – mohsin Jul 08 '20 at 05:42
  • I'm sorry, but you have to give us more than a comment that "it's not working for me". Posting your own new question with an actual code sample that fails will help us give you the help you need. – Chuck H Jul 08 '20 at 16:11
  • This works very well however I don't see any transition. The view just instantly disappears. – alionthego Mar 24 '21 at 23:16
  • Does this help: https://stackoverflow.com/a/59183031/899918 – Chuck H Mar 26 '21 at 18:48
118

SwiftUI Xcode Beta 5

First, declare the @Environment which has a dismiss method which you can use anywhere to dismiss the view.

import SwiftUI

struct GameView: View {
    
    @Environment(\.presentationMode) var presentation
    
    var body: some View {
        Button("Done") {
            self.presentation.wrappedValue.dismiss()
        }
    }
}
David
  • 732
  • 1
  • 6
  • 15
Prashant Gaikwad
  • 3,493
  • 1
  • 24
  • 26
108

iOS 15+

Starting from iOS 15 we can use a new @Environment(\.dismiss):

struct SheetView: View {
    @Environment(\.dismiss) var dismiss

    var body: some View {
        NavigationView {
            Text("Sheet")
                .toolbar {
                    Button("Done") {
                        dismiss()
                    }
                }
        }
    }
}

(There's no more need to use presentationMode.wrappedValue.dismiss().)


Useful links:

pawello2222
  • 46,897
  • 22
  • 145
  • 209
  • 4
    As a relative newcomer to Swift, I'm not logically understanding this pattern *at all*. But it works, and my app is going to be iOS 15 only, so THANK YOU! – Jason Farnsworth Aug 22 '21 at 04:01
  • 1
    This also works great if you want to dismiss a NavigationLink in watchOS. Thanks for directing me to this! – Rob McQualter Aug 11 '22 at 00:03
22

There is now a way to programmatically pop in a NavigationView, if you would like. This is in beta 5. Notice that you don't need the back button. You could programmatically trigger the showSelf property in the DetailView any way you like. And you don't have to display the "Push" text in the master. That could be an EmptyView(), thereby creating an invisible segue.

import SwiftUI

struct ContentView: View {
    var body: some View {
        NavigationView {
            MasterView()
        }
    }
}

struct MasterView: View {
    @State private var showDetail = false

    var body: some View {
        VStack {
            NavigationLink(destination: DetailView(showSelf: $showDetail), isActive: $showDetail) {
                Text("Push")
            }
        }
    }
}

struct DetailView: View {
    @Binding var showSelf: Bool

    var body: some View {
        Button(action: {
            self.showSelf = false
        }) {
            Text("Pop")
        }
    }
}

#if DEBUG
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
#endif
pjtnt11
  • 495
  • 2
  • 5
  • 22
MScottWaller
  • 3,321
  • 2
  • 24
  • 47
  • This is causing errors for me if I press the back button from the detailView's navigation view instead of pressing the 'pop' button. Any ideas how to fix? – MobileMon Aug 19 '19 at 13:35
  • In cases where you use this method you'll want to hide the back button so that it doesn't interfere with your programmatic way of popping the view. Not really a fix, but definitely a way to avoid the issue. – MScottWaller Aug 19 '19 at 18:39
  • I'm hoping beta 6 fixes the issue – MobileMon Aug 19 '19 at 18:48
  • To add this great answer, if you are using the `tag: , selection: ` initialization instead of the `isActive: ` one, you can also pass this selection as a binding variable and set it to nil (or some value other than your tag) to pop the view. – AlexMath Jul 08 '20 at 17:45
  • 2
    Addendum to my comment. This was a huge lesson for me: to be able to pop 2 or more, you need to add `.isDetailLink(false)` to the root navigation link. Otherwise the selection gets set to nil automatically when the 3rd view in the stack appears. – AlexMath Jul 08 '20 at 20:43
10

I recently created an open source project called swiftui-navigation-stack (https://github.com/biobeats/swiftui-navigation-stack) that contains the NavigationStackView, an alternative navigation stack for SwiftUI. It offers several features described in the readme of the repo. For example, you can easily push and pop views programmatically. I'll show you how to do that with a simple example:

First of all embed your hierarchy in a NavigationStackVew:

struct RootView: View {
    var body: some View {
        NavigationStackView {
            View1()
        }
    }
}

NavigationStackView gives your hierarchy access to a useful environment object called NavigationStack. You can use it to, for instance, pop views programmatically as asked in the question above:

struct View1: View {
    var body: some View {
        ZStack {
            Color.yellow.edgesIgnoringSafeArea(.all)
            VStack {
                Text("VIEW 1")
                Spacer()

                PushView(destination: View2()) {
                    Text("PUSH TO VIEW 2")
                }
            }
        }
    }
}

struct View2: View {
    @EnvironmentObject var navStack: NavigationStack
    var body: some View {
        ZStack {
            Color.green.edgesIgnoringSafeArea(.all)
            VStack {
                Text("VIEW 2")
                Spacer()

                Button(action: {
                    self.navStack.pop()
                }, label: {
                    Text("PROGRAMMATICALLY POP TO VIEW 1")
                })
            }
        }
    }
}

In this example I use the PushView to trigger the push navigation with a tap. Then, in the View2 I use the environment object to programmatically come back.

Here is the complete example:

import SwiftUI
import NavigationStack

struct RootView: View {
    var body: some View {
        NavigationStackView {
            View1()
        }
    }
}

struct View1: View {
    var body: some View {
        ZStack {
            Color.yellow.edgesIgnoringSafeArea(.all)
            VStack {
                Text("VIEW 1")
                Spacer()

                PushView(destination: View2()) {
                    Text("PUSH TO VIEW 2")
                }
            }
        }
    }
}

struct View2: View {
    @EnvironmentObject var navStack: NavigationStack
    var body: some View {
        ZStack {
            Color.green.edgesIgnoringSafeArea(.all)
            VStack {
                Text("VIEW 2")
                Spacer()

                Button(action: {
                    self.navStack.pop()
                }, label: {
                    Text("PROGRAMMATICALLY POP TO VIEW 1")
                })
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        RootView()
    }
}

the result is:

enter image description here

superpuccio
  • 11,674
  • 8
  • 65
  • 93
  • 2
    Just tried this out and it is fantastic, it is so much more reliable than the built in SwiftUI stack. I am recursively pushing up to 20 copies of a screen on to the stack and the inbuilt one is getting confused, your NavigationStack handles it perfectly. – Brett Mar 01 '20 at 23:29
  • How can we pop only 2 screens using self.navStack.pop() – AliRehman7141 Feb 24 '21 at 12:00
  • 1
    @AliRehman If you want to pop to a specific view (and not just the previous one) you have to give that view an identifier. For example: `PushView(destination: Child0(), destinationId: "destinationId") { Text("PUSH") }` and then `PopView(destination: .view(withId: "destinationId")) { Text("POP") }`. The same if you programmatically access the navigation stack environment object. – superpuccio Feb 24 '21 at 16:49
  • @matteopuc Thanks I called two times to pop 2 screens. like self.navStack.pop(); self.navStack.pop(); and it was working now updated it according to your suggestion! – AliRehman7141 Mar 09 '21 at 04:04
  • 1
    Everyone should use this if you want to use routers (highly recommend) or programmatically pushing and popping. Thanks for this easy-to-understand, elegant solution to SwiftUI navigation, no magic involved :) – Andrew May 06 '22 at 18:30
7

Alternatively, if you don't want to do it programatically from a button, you can emit from the view model whenever you need to pop. Subscribe to a @Published that changes the value whenever the saving is done.

struct ContentView: View {
    @ObservedObject var viewModel: ContentViewModel
    @Environment(\.presentationMode) var presentationMode

    init(viewModel: ContentViewModel) {
        self.viewModel = viewModel
    }

    var body: some View {
        Form {
            TextField("Name", text: $viewModel.name)
                .textContentType(.name)
        }
        .onAppear {
            self.viewModel.cancellable = self.viewModel
                .$saved
                .sink(receiveValue: { saved in
                    guard saved else { return }
                    self.presentationMode.wrappedValue.dismiss()
                }
            )
        }
    }
}

class ContentViewModel: ObservableObject {
    @Published var saved = false // This can store any value.
    @Published var name = ""
    var cancellable: AnyCancellable? // You can use a cancellable set if you have multiple observers.

    func onSave() {
        // Do the save.

        // Emit the new value.
        saved = true
    }
}
Dan Bodnar
  • 487
  • 6
  • 9
6

Please check Following Code it's so simple.

FirstView

struct StartUpVC: View {
@State var selection: Int? = nil

var body: some View {
    NavigationView{
        NavigationLink(destination: LoginView().hiddenNavigationBarStyle(), tag: 1, selection: $selection) {
            Button(action: {
                print("Signup tapped")
                self.selection = 1
            }) {
                HStack {
                    Spacer()
                    Text("Sign up")
                    Spacer()
                }
            }
        }
    }
}

SecondView

struct LoginView: View {
@Environment(\.presentationMode) var presentationMode
    
var body: some View {
    NavigationView{
        Button(action: {
           print("Login tapped")
           self.presentationMode.wrappedValue.dismiss()
        }) {
           HStack {
              Image("Back")
              .resizable()
              .frame(width: 20, height: 20)
              .padding(.leading, 20)
           }
        }
      }
   }
}
Super Developer
  • 891
  • 11
  • 20
  • This is the best answer now, but note instead of using presentationMode, you can just pass in an environment object that watches selection, and set selection to nil from any subsequent child view to go back to root view. – Super Noob Sep 07 '20 at 05:45
3

You can try using a custom view and a Transition.

Here's a custom modal.

struct ModalView<Content>: View where Content: View {

    @Binding var isShowing: Bool
    var content: () -> Content

    var body: some View {
        GeometryReader { geometry in
            ZStack(alignment: .center) {
                if (!self.isShowing) {
                    self.content()
                }
                if (self.isShowing) {
                    self.content()
                        .disabled(true)
                        .blur(radius: 3)

                    VStack {
                        Text("Modal")
                    }
                    .frame(width: geometry.size.width / 2,
                           height: geometry.size.height / 5)
                    .background(Color.secondary.colorInvert())
                    .foregroundColor(Color.primary)
                    .cornerRadius(20)
                    .transition(.moveAndFade) // associated transition to the modal view
                }
            }
        }
    }

}

I reused the Transition.moveAndFade from the Animation Views and Transition tutorial.

It is defined like this:

extension AnyTransition {
    static var moveAndFade: AnyTransition {
        let insertion = AnyTransition.move(edge: .trailing)
            .combined(with: .opacity)
        let removal = AnyTransition.scale()
            .combined(with: .opacity)
        return .asymmetric(insertion: insertion, removal: removal)
    }
}

You can test it - in the simulator, not in the preview - like this:

struct ContentView: View {

    @State var isShowingModal: Bool = false

    func toggleModal() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
            withAnimation {
                self.isShowingModal = true
            }
            DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
                withAnimation {
                    self.isShowingModal = false
                }
            }
        }
    }

    var body: some View {
        ModalView(isShowing: $isShowingModal) {
            NavigationView {
                List(["1", "2", "3", "4", "5"].identified(by: \.self)) { row in
                    Text(row)
                }.navigationBarTitle(Text("A List"), displayMode: .large)
            }.onAppear { self.toggleModal() }
        }
    }

}

Thanks to that transition, you will see the modal sliding in from the trailing edge, and the it will zoom and fade out when it is dismissed.

Matteo Pacini
  • 21,796
  • 7
  • 67
  • 74
  • 1
    Thanks Matteo, I will try this as soon as possible, this could be a cool temporary workaround hoping apple will introduce dismiss and pop – Andrea Miotto Jun 09 '19 at 10:26
1

The core concept of SwiftUI is to watch over the data flow.

You have to use a @State variable and mutate the value of this variable to control popping and dismissal.

struct MyView: View {
    @State
    var showsUp = false

    var body: some View {
        Button(action: { self.showsUp.toggle() }) {
            Text("Pop")
        }
        .presentation(
            showsUp ? Modal(
                Button(action: { self.showsUp.toggle() }) {
                    Text("Dismiss")
                }
            ) : nil
        )
    }
}

WeZZard
  • 3,536
  • 1
  • 23
  • 26
  • What if the user close the modal swiping down? the state stays in a wrong state. And there is not a way to add a listener to the swipe down gesture. I''m pretty sure the will extends this pop/dismiss features with the next releases – Andrea Miotto Jun 16 '19 at 15:54
  • Try `onDisappear(_:)` ? – WeZZard Jun 18 '19 at 01:23
0

I experienced a compiler issue trying to call value on the presentationMode binding. Changing the property to wrappedValue fixed the issue for me. I'm assuming value -> wrappedValue is a language update. I think this note would be more appropriate as a comment on Chuck H's answer but don't have enough rep points to comment, I also suggested this change as and edit but my edit was rejected as being more appropriate as a comment or answer.

gacoler
  • 11
  • 2
0

This will also dismiss the view

       let scenes = UIApplication.shared.connectedScenes
                let windowScene = scenes.first as? UIWindowScene
                let window = windowScene?.windows.first
                
                window?.rootViewController?.dismiss(animated: true, completion: {
                    print("dismissed")
                })
Abdullah
  • 231
  • 2
  • 11