55

I am trying to push from login view to detail view but not able to make it.even navigation bar is not showing in login view. How to push on button click in SwiftUI? How to use NavigationLink on button click?

var body: some View {
    
    NavigationView {
        VStack(alignment: .leading) {
            Text("Let's get you signed in.")
                .bold()
                .font(.system(size: 40))
                .multilineTextAlignment(.leading)
                .frame(width: 300, height: 100, alignment: .topLeading)
                .padding(Edge.Set.bottom, 50)
            
            Text("Email address:")
                .font(.headline)
            TextField("Email", text: $email)
                .frame(height:44)
                .accentColor(Color.white)
                .background(Color(UIColor.darkGray))
                .cornerRadius(4.0)
            
            Text("Password:")
                .font(.headline)
            
            SecureField("Password", text: $password)
                .frame(height:44)
                .accentColor(Color.white)
                .background(Color(UIColor.darkGray))
                .cornerRadius(4.0)
            
            Button(action: {
                print("login tapped")
            }) {
                HStack {
                    Spacer()
                    Text("Login").foregroundColor(Color.white).bold()
                    Spacer()
                }
            }
            .accentColor(Color.black)
            .padding()
            .background(Color(UIColor.darkGray))
            .cornerRadius(4.0)
            .padding(Edge.Set.vertical, 20)
        }
        .padding(.horizontal,30)
    }
    .navigationBarTitle(Text("Login"))
}
aheze
  • 24,434
  • 8
  • 68
  • 125
Rock
  • 1,408
  • 1
  • 12
  • 27

7 Answers7

56

To fix your issue you need to bind and manage tag with NavigationLink, So create one state inside you view as follow, just add above body.

@State var selection: Int? = nil

Then update your button code as follow to add NavigationLink

NavigationLink(destination: Text("Test"), tag: 1, selection: $selection) {
    Button(action: {
        print("login tapped")
        self.selection = 1
    }) {
        HStack {
            Spacer()
            Text("Login").foregroundColor(Color.white).bold()
            Spacer()
        }
    }
    .accentColor(Color.black)
    .padding()
    .background(Color(UIColor.darkGray))
    .cornerRadius(4.0)
    .padding(Edge.Set.vertical, 20)
}

Meaning is, when selection and NavigationLink tag value will match then navigation will be occurs.

I hope this will help you.

Sagar Chauhan
  • 5,715
  • 2
  • 22
  • 56
  • i tried to use it. but how actually write it on button click. because i am getting errors. – Rock Sep 05 '19 at 06:50
  • 1
    You don't actually need the *tag+selection* variant. There is a simpler *isActive* variant - see [this answer](https://stackoverflow.com/a/63367285/8697793) – pawello2222 Aug 11 '20 at 22:52
  • 1
    @pawello2222 `isActive` seems to be hard to get working when you want the navigation to be triggered by a button click. I find the method in this answer perfect for such a use case. – mota Jan 26 '21 at 20:41
  • this is working only when having navigationLink inside NavigationView. – Saurabh Prajapati Sep 07 '22 at 09:31
55

iOS 16+

Note: Below is a simplified example of how to present a new view. For a more advanced generic example please see this answer.

In iOS 16 we can access the NavigationStack and NavigationPath.

Usage #1

A new view is activated by a simple NavigationLink:

struct ContentView: View {
    var body: some View {
        NavigationStack {
            NavigationLink(value: "NewView") {
                Text("Show NewView")
            }
            .navigationDestination(for: String.self) { view in
                if view == "NewView" {
                    Text("This is NewView")
                }
            }
        }
    }
}

Usage #2

A new view is activated by a standard Button:

struct ContentView: View {
    @State private var path = NavigationPath()

    var body: some View {
        NavigationStack(path: $path) {
            Button {
                path.append("NewView")
            } label: {
                Text("Show NewView")
            }
            .navigationDestination(for: String.self) { view in
                if view == "NewView" {
                    Text("This is NewView")
                }
            }
        }
    }
}

Usage #3

A new view is activated programmatically:

struct ContentView: View {
    @State private var path = NavigationPath()

    var body: some View {
        NavigationStack(path: $path) {
            Text("Content View")
                .navigationDestination(for: String.self) { view in
                    if view == "NewView" {
                        Text("This is NewView")
                    }
                }
        }
        .onAppear {
            path.append("NewView")
        }
    }
}

iOS 13+

The accepted answer uses NavigationLink(destination:tag:selection:) which is correct.

However, for a simple view with just one NavigationLink you can use a simpler variant: NavigationLink(destination:isActive:)

Usage #1

NavigationLink is activated by a standard Button:

struct ContentView: View {
    @State var isLinkActive = false

    var body: some View {
        NavigationView {
            VStack(alignment: .leading) {
                ...
                NavigationLink(destination: Text("OtherView"), isActive: $isLinkActive) {
                    Button(action: {
                        self.isLinkActive = true
                    }) {
                        Text("Login")
                    }
                }
            }
            .navigationBarTitle(Text("Login"))
        }
    }
}

Usage #2

NavigationLink is hidden and activated by a standard Button:

struct ContentView: View {
    @State var isLinkActive = false

    var body: some View {
        NavigationView {
            VStack(alignment: .leading) {
                ...
                Button(action: {
                    self.isLinkActive = true
                }) {
                    Text("Login")
                }
            }
            .navigationBarTitle(Text("Login"))
            .background(
                NavigationLink(destination: Text("OtherView"), isActive: $isLinkActive) {
                    EmptyView()
                }
                .hidden()
            )
        }
    }
}

Usage #3

NavigationLink is hidden and activated programmatically:

struct ContentView: View {
    @State var isLinkActive = false

    var body: some View {
        NavigationView {
            VStack(alignment: .leading) {
                ...
            }
            .navigationBarTitle(Text("Login"))
            .background(
                NavigationLink(destination: Text("OtherView"), isActive: $isLinkActive) {
                    EmptyView()
                }
                .hidden()
            )
        }
        .onAppear {
            self.isLinkActive = true
        }
    }
}

Here is a GitHub repository with different SwiftUI extensions that makes navigation easier.

pawello2222
  • 46,897
  • 22
  • 145
  • 209
  • 4
    A hidden NavigationLink to control programmatic segues was a revelation. Thank you! – LordParsley Dec 10 '20 at 10:10
  • Usage #2 worked when using the new iOS Menu feature to programmatically go to a NavLink. @pawello2222 - genius. – FontFamily Dec 22 '20 at 18:25
  • 3
    Wow, Apple really made NavigationView confusing. – ScottyBlades Apr 25 '21 at 02:54
  • Wait, Is `NavigationLink(destination:isActive:)` not suitable for the view that has more than 1 NavigationLinks? – user482594 May 20 '21 at 11:09
  • 1
    @user482594 You can easily use multiple `NavigationLink(destination:isActive:)` in one view - controlled by multiple `@State` variables. This works best if the links are independent from each other. However, the `NavigationLink(destination:tag:selection:)` may be more suitable for cases when you need to add multiple *related* NavigationLinks - like in a list etc. – pawello2222 May 20 '21 at 11:37
  • Thanks for the examples. It still really blows my mind how Apple manages to make something this trivial as changing views as ugly as this. They always manage. – Azurlake Nov 15 '21 at 06:01
  • @Azurlake yeah this is ridiculous, you should be able to navigate to a different view from a button press (self.present(MyNewView())), much simpler than using `@State` properties everywhere – NoKey Feb 16 '22 at 12:52
10

Another approach:

SceneDelegate

if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(rootView: BaseView().environmentObject(ViewRouter()))
            self.window = window
            window.makeKeyAndVisible()
        }

BaseView

import SwiftUI

struct BaseView : View {

    @EnvironmentObject var viewRouter: ViewRouter

    var body: some View {
        VStack {
            if viewRouter.currentPage == "view1" {
                FirstView()
            } else if viewRouter.currentPage == "view2" {
                SecondView()
                    .transition(.scale)
            }
        }
    }
}

#if DEBUG
struct MotherView_Previews : PreviewProvider {
    static var previews: some View {
        BaseView().environmentObject(ViewRouter())
    }
}
#endif

ViewRouter

import Foundation
import Combine
import SwiftUI

class ViewRouter: ObservableObject {

    let objectWillChange = PassthroughSubject<ViewRouter,Never>()

    var currentPage: String = "view1" {
        didSet {
            withAnimation() {
                objectWillChange.send(self)
            }
        }
    }
}

FirstView

import SwiftUI

struct FirstView : View {

    @EnvironmentObject var viewRouter: ViewRouter

    var body: some View {
        VStack {
            Button(action: {self.viewRouter.currentPage = "view2"}) {
                NextButtonContent()
            }
        }
    }
}

#if DEBUG
struct FirstView_Previews : PreviewProvider {
    static var previews: some View {
        FirstView().environmentObject(ViewRouter())
    }
}
#endif

struct NextButtonContent : View {
    var body: some View {
        return Text("Next")
            .foregroundColor(.white)
            .frame(width: 200, height: 50)
            .background(Color.blue)
            .cornerRadius(15)
            .padding(.top, 50)
    }
}

SecondView

import SwiftUI

struct SecondView : View {

    @EnvironmentObject var viewRouter: ViewRouter

    var body: some View {
        VStack {
            Spacer(minLength: 50.0)
            Button(action: {self.viewRouter.currentPage = "view1"}) {
                BackButtonContent()
            }
        }
    }
}

#if DEBUG
struct SecondView_Previews : PreviewProvider {
    static var previews: some View {
        SecondView().environmentObject(ViewRouter())
    }
}
#endif

struct BackButtonContent : View {
    var body: some View {
        return Text("Back")
            .foregroundColor(.white)
            .frame(width: 200, height: 50)
            .background(Color.blue)
            .cornerRadius(15)
            .padding(.top, 50)
    }
}

Hope this helps!

Dharmesh Mansata
  • 4,422
  • 1
  • 27
  • 33
1

In my opinion a cleaner way for iOS 16+ is using a state bool to present the view.

struct ButtonNavigationView: View {
    @State private var isShowingSecondView : Bool = false
    
    var body: some View {
        NavigationStack {
            VStack{
                Button(action:{isShowingSecondView = true} ){
                    Text("Show second view")
                }
            }.navigationDestination(isPresented: $isShowingSecondView) {
                Text("SecondView")
            }
        }
    }
}
yawnobleix
  • 1,204
  • 10
  • 21
1

We should use NavigationStack instead of NavigationView, that's the new way to handle the navigation using the button in SwiftUI.

You need to use the state property wrapper for navigation as follows

@State private var isShowingDashboardView = false

then use the following code.

    NavigationStack {
      VStack{
         Button(action: {
           isShowingDashboardView = true
         }) {
           Text("Login")
         }
      }
       .navigationDestination(isPresented: $isShowingDashboardView, destination: { DashboardView()})
    }
0

Simplest and most effective solution is :

NavigationLink(destination:ScoresTableView()) {
                    Text("Scores")
                }.navigationBarHidden(true)
                    .frame(width: 90, height: 45, alignment: .center)
                    .foregroundColor(.white)
                    .background(LinearGradient(gradient: Gradient(colors: [Color.red, Color.blue]), startPoint: .leading, endPoint: .trailing))
                    .cornerRadius(10)
                    .contentShape(Rectangle())
                    .padding(EdgeInsets(top: 16, leading: UIScreen.main.bounds.size.width - 110 , bottom: 16, trailing: 20))

ScoresTableView is the destination view.

cigien
  • 57,834
  • 11
  • 73
  • 112
Haseeb Javed
  • 1,769
  • 17
  • 20
0

I think above answers are nice, but simpler way should be:

    NavigationLink {
        TargetView()
    } label: {
        Text("Click to go")
    }
losiu
  • 39
  • 2