104

I was playing around with SwiftUI and want to be able to come back to the previous view when tapping a button, the same we use popViewController inside a UINavigationController. Is there a provided way to do it so far ?

I've also tried to use NavigationDestinationLink to do so without success.

struct AView: View {
    var body: some View {
        NavigationView {
            NavigationButton(destination: BView()) {
                Text("Go to B")
            }
        }
    }
}

struct BView: View {
    var body: some View {
        Button(action: {
            // Trying to go back to the previous view
            // previously: navigationController.popViewController(animated: true)
        }) {
            Text("Come back to A")
        }
    }
}
LinusGeffarth
  • 27,197
  • 29
  • 120
  • 174
Alexandre Legent
  • 1,049
  • 2
  • 8
  • 6
  • It's possible that Apple wants to discourage this, so that the back button is the only way to go back. It does seem like an oversight, however. – mginn Jun 07 '19 at 16:46
  • Would it work to conditionally show one view or another? Or does it have to use `NavigationView`? – zoecarver Jun 08 '19 at 05:12
  • @zoecarver The idea was to have this second view to add an item (a blog post for exemple), with a save button to return to main view with my new item or the back button to cancel it. It's just a way I was used to, however I can probably embedded that view in a pop up or something else. – Alexandre Legent Jun 08 '19 at 06:32
  • You could try something like `.presentation(boolCheck ? BView() : nil)` (not sure if that will work though). Where `boolCheck` is a stateful variable. – zoecarver Jun 08 '19 at 06:40
  • Actually, it could be better to have a good looking view on all device with `.presentation` and that's maybe why Apple discourage the pop view behaviour, as @mginn said. – Alexandre Legent Jun 08 '19 at 06:44
  • 7
    It doesn't make sense to me why they'd discourage it. Their native Picker view exhibits a pop behavior when you pick an item in the list. It just seems like the API isn't finalized and that something is missing. – Toni Sučić Jun 09 '19 at 16:43
  • This is answer may help you , https://stackoverflow.com/questions/56571349/custom-back-button-for-navigationviews-navigation-bar-in-swiftui – Mohamed Eltantawy Apr 25 '20 at 02:04
  • For iOS 16 there is a new programmatic navigation option with NavigationStack. look st this answer : https://stackoverflow.com/a/76772102/9497800 – multitudes Aug 11 '23 at 15:58

12 Answers12

100

Modify your BView struct as follows. The button will perform just as popViewController did in UIKit.

struct BView: View {
    @Environment(\.presentationMode) var presentationMode
    
    var body: some View {
        Button("Come back to A") {
            presentationMode.wrappedValue.dismiss()
        }
    }
}
aheze
  • 24,434
  • 8
  • 68
  • 125
Chuck H
  • 7,434
  • 4
  • 31
  • 34
  • This solution worked for me. I also tried the other models but when you use one of the other solutions in a List the detail view got launched as many times as I had records in the list. This is probably caused by the fact that the @state variable is not element specific but is "global" for the master view. – Fred Appelman Aug 14 '19 at 20:03
  • 2
    `value` has changed to `wrappedValue` now. – LinusGeffarth Sep 08 '19 at 20:23
  • 4
    `@Environment(\.presentationMode) var mode` is sufficient, due to type inference. – Alex Brown Oct 17 '19 at 15:59
  • Thanks. You are correct. Even though it is not necessary, I included it for clarity to help someone reading the answer understand what is actually happening. – Chuck H Oct 17 '19 at 16:07
  • is it possible to pop view from view model @ObservedObject ? – theMouk Nov 29 '19 at 13:31
  • IF I understand what you are asking (small example snippet of code and real SO question would really help), you can try having your ObservableObject emit to a publisher and then put an .OnReceive in your View that does the the dismiss. – Chuck H Nov 29 '19 at 18:41
  • Although Xcode 11 errored on @ Environment, I made it work by adding @ SwiftUI.Environment – Vivienne Fosh Dec 19 '19 at 15:29
  • Any idea how to make this work for macOS (not Catalyst)? As documented here, the same approach doesn't seem to work: https://stackoverflow.com/questions/59361491/swiftui-dismiss-view-within-macos-navigationview – TheNeil Feb 01 '20 at 15:30
  • I'm trying this with custom back button but it seems not working. I have created a separate struct BackButton to use it in whole project and defined enviroment variable presentationMode but it seems not working. – Saurabh Prajapati Sep 10 '20 at 06:16
  • `struct BackButton: View { @Environment(\.presentationMode) var presentationMode var body: some View { Button(action: { print("back tapped") print(self.presentationMode.wrappedValue) self.presentationMode.wrappedValue.dismiss() }) { Image("aIc24ArrowTailLeft") .frame(width: 40, height: 40) .foregroundColor(.textColor) } } }` – Saurabh Prajapati Sep 10 '20 at 06:18
  • This doesn't work for wrapped SwiftUI views that were pushed onto the navstack from UIView – Aaron Bratcher Apr 29 '21 at 13:32
21

Use @Environment(\.presentationMode) var presentationMode to go back previous view. Check below code for more understanding.

import SwiftUI

struct ContentView: View {


    var body: some View {

        NavigationView {
            ZStack {
                Color.gray.opacity(0.2)

                NavigationLink(destination: NextView(), label: {Text("Go to Next View").font(.largeTitle)})
            }.navigationBarTitle(Text("This is Navigation"), displayMode: .large)
                .edgesIgnoringSafeArea(.bottom)
        }
    }
}

struct NextView: View {
    @Environment(\.presentationMode) var presentationMode
    var body: some View {
        ZStack {
            Color.gray.opacity(0.2)
        }.navigationBarBackButtonHidden(true)
            .navigationBarItems(leading: Button(action: {
                self.presentationMode.wrappedValue.dismiss()
            }, label: { Image(systemName: "arrow.left") }))
            .navigationBarTitle("", displayMode: .inline)
    }
}


struct NameRow: View {
    var name: String
    var body: some View {
        HStack {
            Image(systemName: "circle.fill").foregroundColor(Color.green)
            Text(name)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
Ashish
  • 706
  • 8
  • 22
15

With State Variables. Try that.

struct ContentViewRoot: View {
    @State var pushed: Bool = false
    var body: some View {
        NavigationView{
            VStack{
                NavigationLink(destination:ContentViewFirst(pushed: self.$pushed), isActive: self.$pushed) { EmptyView() }
                    .navigationBarTitle("Root")
                Button("push"){
                    self.pushed = true
                }
            }
        }
        .navigationViewStyle(StackNavigationViewStyle())
    }
}


struct ContentViewFirst: View {
    @Binding var pushed: Bool
    @State var secondPushed: Bool = false
    var body: some View {
        VStack{
            NavigationLink(destination: ContentViewSecond(pushed: self.$pushed, secondPushed: self.$secondPushed), isActive: self.$secondPushed) { EmptyView() }
                .navigationBarTitle("1st")
            Button("push"){
                self.secondPushed = true;
            }
        }
    }
}



struct ContentViewSecond: View {
    @Binding var pushed: Bool
    @Binding var secondPushed: Bool

    var body: some View {
        VStack{
            Spacer()
            Button("PopToRoot"){
                self.pushed = false
            } .navigationBarTitle("2st")

            Spacer()
            Button("Pop"){
                         self.secondPushed = false
                     } .navigationBarTitle("1st")
            Spacer()
        }
    }
}

enter image description here

  • 2
    This is a great answer, I just want to add that if you have this in a form or list, the NavigationLink with the EmptyView will occupy one row. So to avoid an empty row, one should put the NavigationLink and the Button in a ZStack. – GJ Nilsen Mar 01 '20 at 22:02
10

This seems to work for me on watchOS (haven't tried on iOS):

@Environment(\.presentationMode) var presentationMode

And then when you need to pop

self.presentationMode.wrappedValue.dismiss()
Cherpak Evgeny
  • 2,659
  • 22
  • 29
8

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.

(The new NavigationLink functionality takes over the deprecated NavigationDestinationLink)

import SwiftUI

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

struct MasterView: View {
    @State 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
M-P
  • 4,909
  • 3
  • 25
  • 31
MScottWaller
  • 3,321
  • 2
  • 24
  • 47
  • 2
    This approach will work only if the Pop button is used. If the Back button is tapped, the showSelf var does not get toggled back to false and the MasterView will automatically push the DetailView again. See my answer below for an overall cleaner solution. – Chuck H Aug 02 '19 at 19:35
  • 1
    I found this worked for me. I can use the back button or the Pop button and get the expected result. – dfrobison Feb 09 '20 at 00:42
4

It seems that a ton of basic navigation functionality is super buggy, which is disappointing and may be worth walking away from for now to save hours of frustration. For me, PresentationButton is the only one that works. TabbedView tabs don't work properly, and NavigationButton doesn't work for me at all. Sounds like YMMV if NavigationButton works for you.

I'm hoping that they fix it at the same time they fix autocomplete, which would give us much better insight as to what is available to us. In the meantime, I'm reluctantly coding around it and keeping notes for when fixes come out. It sucks to have to figure out if we're doing something wrong or if it just doesn't work, but that's beta for you!

Sean
  • 340
  • 3
  • 11
  • Thanks for your answer. I don't seem to have the many trouble that you experience. However, this is still beta as you said, and isn't finalised, but `PresentationButton` do the trick for me right now. – Alexandre Legent Jun 11 '19 at 08:08
3

Update: the NavigationDestinationLink API in this solution has been deprecated as of iOS 13 Beta 5. It is now recommended to use NavigationLink with an isActive binding.

I figured out a solution for programmatic pushing/popping of views in a NavigationView using NavigationDestinationLink.

Here's a simple example:

import Combine
import SwiftUI

struct DetailView: View {
    var onDismiss: () -> Void

    var body: some View {
        Button(
            "Here are details. Tap to go back.",
            action: self.onDismiss
        )
    }
}

struct MainView: View {
    var link: NavigationDestinationLink<DetailView>
    var publisher: AnyPublisher<Void, Never>

    init() {
        let publisher = PassthroughSubject<Void, Never>()
        self.link = NavigationDestinationLink(
            DetailView(onDismiss: { publisher.send() }),
            isDetail: false
        )
        self.publisher = publisher.eraseToAnyPublisher()
    }

    var body: some View {
        VStack {
            Button("I am root. Tap for more details.", action: {
                self.link.presented?.value = true
            })
        }
            .onReceive(publisher, perform: { _ in
                self.link.presented?.value = false
            })
    }
}

struct RootView: View {
    var body: some View {
        NavigationView {
            MainView()
        }
    }
}

I wrote about this in a blog post here.

Ryan Ashcraft
  • 454
  • 3
  • 7
  • 1
    beta 4 error: Value of type 'AnyPublisher' has no member 'send' – MobileMon Jul 19 '19 at 12:53
  • @Ryan Ashcraft .! This senario does not work when we have TextField and button. If textfield is already filled, navigation works, but when ever we write on TextField and press button, Application got hanged and nothing happened, our actions does not worked. My scenario is for login view.. – Muhammad Danish Qureshi Aug 08 '19 at 13:40
3

You can also do it with .sheet

.navigationBarItems(trailing: Button(action: {
            self.presentingEditView.toggle()
        }) {
            Image(systemName: "square.and.pencil")
        }.sheet(isPresented: $presentingEditView) {
            EditItemView()
        })

In my case I use it from a right navigation bar item, then you have to create the view (EditItemView() in my case) that you are going to display in that modal view.

https://developer.apple.com/documentation/swiftui/view/sheet(ispresented:ondismiss:content:)

1

EDIT: This answer over here is better than mine, but both work: SwiftUI dismiss modal

What you really want (or should want) is a modal presentation, which several people have mentioned here. If you go that path, you definitely will need to be able to programmatically dismiss the modal, and Erica Sadun has a great example of how to do that here: https://ericasadun.com/2019/06/16/swiftui-modal-presentation/

Given the difference between declarative coding and imperative coding, the solution there may be non-obvious (toggling a bool to false to dismiss the modal, for example), but it makes sense if your model state is the source of truth, rather than the state of the UI itself.

Here's my quick take on Erica's example, using a binding passed into the TestModal so that it can dismiss itself without having to be a member of the ContentView itself (as Erica's is, for simplicity).

struct TestModal: View {
    @State var isPresented: Binding<Bool>

    var body: some View {
        Button(action: { self.isPresented.value = false }, label: { Text("Done") })
    }
}

struct ContentView : View {
    @State var modalPresented = false

    var body: some View {
        NavigationView {
            Text("Hello World")
            .navigationBarTitle(Text("View"))
            .navigationBarItems(trailing:
                Button(action: { self.modalPresented = true }) { Text("Show Modal") })
        }
        .presentation(self.modalPresented ? Modal(TestModal(isPresented: $modalPresented)) {
            self.modalPresented.toggle()
        } : nil)
    }
}
Brad
  • 655
  • 2
  • 7
  • 13
1

Below works for me in XCode11 GM

self.myPresentationMode.wrappedValue.dismiss()
guru
  • 2,727
  • 3
  • 27
  • 39
0

instead of NavigationButton use Navigation DestinationLink

but You should import Combine

struct AView: View {
 var link: NavigationDestinationLink<BView>
var publisher: AnyPublisher<Void, Never>

init() {
    let publisher = PassthroughSubject<Void, Never>()
    self.link = NavigationDestinationLink(
        BView(onDismiss: { publisher.send() }),
        isDetail: false
    )
    self.publisher = publisher.eraseToAnyPublisher()
}

var body: some View {
    NavigationView {
        Button(action:{
        self.link.presented?.value = true


 }) {
            Text("Go to B")
        }.onReceive(publisher, perform: { _ in
            self.link.presented?.value = false
        })
    }
}
}

struct BView: View {
var onDismiss: () -> Void
var body: some View {
    Button(action: self.onDismiss) {
        Text("Come back to A")
    }
}
}
Alireza12t
  • 377
  • 1
  • 4
  • 14
-1

In the destination pass the view you want to redirect, and inside block pass data you to pass in another view.

NavigationLink(destination: "Pass the particuter View") {
    Text("Push")
}
Dharman
  • 30,962
  • 25
  • 85
  • 135