101

I've got a basic view with a button using SwiftUI and I'm trying to present a new screen/view when the button is tapped. How do I do this? Am I suppose to create a delegate for this view that will tell the app's SceneDelegate to present a new view controller?

import SwiftUI

struct ContentView : View {
    var body: some View {
        VStack {
            Text("Hello World")
            Button(action: {
                //go to another view
            }) {
                Text("Do Something")
                    .font(.largeTitle)
                    .fontWeight(.ultraLight)
            }
        }
    }
}
Jake
  • 13,097
  • 9
  • 44
  • 73

15 Answers15

87

The key is to use a NavigationView and a NavigationLink:

import SwiftUI

struct ContentView : View {
    var body: some View {
        NavigationView {
            VStack {
                Text("Hello World")
                NavigationLink(destination: DetailView()) {
                    Text("Do Something")
                }
            }
        }
    }
}
Jake
  • 13,097
  • 9
  • 44
  • 73
  • 1
    NB At the moment this approach isn't working on Apple Watch because 'NavigationView' is unavailable in watchOS. You can use only NavigationLink as explained [here](https://onmyway133.github.io/blog/How-to-do-navigation-in-SwiftUI-in-watchOS/) – sgiraz Dec 01 '19 at 19:54
  • 1
    This does work, but it tightly couples these views, is there a better approach where one view doesn't know about the other? – Zorayr May 06 '20 at 02:42
  • 37
    Is there a way to do this with a `Button` instead of an ugly NavigationLink? – Andrew K Nov 27 '20 at 23:49
  • 1
    https://www.hackingwithswift.com/quick-start/swiftui/how-to-use-programmatic-navigation-in-swiftui – MVZ Mar 04 '22 at 13:24
  • For anyone who already has the UI layout done, this is an unacceptable amount of UI refactoring for something that should be simple. The answer by "George" is way more usable for an existing UI. – Andrew Koster Jan 26 '23 at 23:02
78

This was not intended to be the top answer here - but rather an alternative method if you didn't want the navigation bar. See Jake's answer below for the normal way to do this with NavigationView & NavigationLink. Hopefully this is still useful or points you in the right direction too.

If you are just trying to get a NavigationLink working with a Binding, you can use the same NavigationLink init I did with the isActive binding parameter.

Anyway, back to the answer…


I made a view modifier for this. It also means that there is no navigation bar. You can call it like so:

.navigate(to: MainPageView(), when: $willMoveToNextScreen)

This can be attached to anything, so I typically attach it to the end of the body, for example:

@State private var willMoveToNextScreen = false

var body: some View {
    VStack {
        /* ... */
    }
    .navigate(to: MainPageView(), when: $willMoveToNextScreen)
}

Code (remember to import SwiftUI):

extension View {
    /// Navigate to a new view.
    /// - Parameters:
    ///   - view: View to navigate to.
    ///   - binding: Only navigates when this condition is `true`.
    func navigate<NewView: View>(to view: NewView, when binding: Binding<Bool>) -> some View {
        NavigationView {
            ZStack {
                self
                    .navigationBarTitle("")
                    .navigationBarHidden(true)

                NavigationLink(
                    destination: view
                        .navigationBarTitle("")
                        .navigationBarHidden(true),
                    isActive: binding
                ) {
                    EmptyView()
                }
            }
        }
        .navigationViewStyle(.stack)
    }
}
Tiago Mendes
  • 4,572
  • 1
  • 29
  • 35
George
  • 25,988
  • 10
  • 79
  • 133
  • 1
    neat approach, although it seemed to break my pre-existing views with styling already applied. – ty1 Mar 22 '20 at 08:05
  • very concise, neat. best – Ktt Apr 14 '20 at 10:08
  • 2
    Nice work. One more thing, any chance to change the navigation animation? It was always left-to-right by default. – sakiM Jul 30 '20 at 07:25
  • @sakiM Which way would you animate it? This is default and a standard so I just left it like this. If you want it to come from the bottom, I would suggest `.sheet` instead. Don’t confuse users with an unexpected behaviour – George Jul 30 '20 at 09:04
  • Unfortunately in Xcode 12.2 this works, but puts a small grey rectangle in the middle of the originating screen. So it's no longer viable. – Plasma Nov 22 '20 at 01:00
  • @Plasma That’s seems very weird. Can you upload to imgur and reply with link so I can see? – George Nov 22 '20 at 01:35
  • @George_E here you go .. the small grey round rect appears immediately after adding .navigate(to: MainPageView(), when: $willMoveToNextScreen) to the bottom of my view, if I comment this line out the round rect disappears..... https://imgur.com/r4lblhn – Plasma Nov 22 '20 at 12:06
  • @Plasma Try use this in a blank project. It really doesn't seem related at all to me. It could be an issue with the previews (try simulator), but if not it is likely something in your code you have accidentally missed out. It almost looks like `.redacted(reason: .placeholder)` to me but I'm not sure – George Nov 22 '20 at 12:34
  • @George_E after a fair bit of testing, seems to only be a problem if the target is tvOS, works fine on IOS. But when set to tvOS the grey rectangle is visible in the preview, simulator and on a real device. – Plasma Nov 22 '20 at 18:39
  • 2
    I m getting `Unable to simultaneously satisfy constraints. Probably at least one of the constraints in the following list is one you don't want` with every `.navigate()` – Rahul Gaur Mar 25 '21 at 11:23
  • This is amazing. thanks for sharing this. – drago May 08 '21 at 13:58
  • Hey @George i am also having the same issue that Rauhul Gaur is having using the iOS 15. With iOS 14 I don't have any errors. – Tiago Mendes Nov 18 '21 at 17:23
  • @RahulGaur did you find a way to fix the constraints errors? – Tiago Mendes Nov 18 '21 at 17:24
  • 2
    @TiagoMendes Adding a `.navigationViewStyle(.stack)` to the `NavigationView` will probably fix it – George Nov 18 '21 at 17:27
  • @George thank you that did solved the problem – Tiago Mendes Nov 18 '21 at 17:34
  • 2
    That's a good answer for below IOS 16 but isActive depreciated in IOS 16 – Abdullah Jul 11 '22 at 13:36
  • This is the only acceptable answer. Everything else involves an insane amount of UI refactoring for something that should only take a single line of code. – Andrew Koster Jan 26 '23 at 23:00
  • Thanks, great for apps that still have to support pre-iOS 16 and also aren't looking into using some third-party library. – Alexandre Jun 20 '23 at 18:51
13

if don't want to show the navigationView you can hide it in destination.

struct ContentViewA : View {
    var body: some View {
        NavigationView {
            VStack {
                Text("Hello World")
                NavigationLink(destination: ContentViewB()) {
                    Text("Go To Next Step")
                }
            }
        }
    }
}



struct ContentViewB : View {
        var body: some View {
            NavigationView {
                VStack {
                    Text("Hello World B")

                }.navigationBarTitle("")
                .navigationBarHidden(true)
            }
        }
    }

or if you want to hide it based on conditions you can use @State for changing the visibility.

Mohsen mokhtari
  • 2,841
  • 1
  • 30
  • 39
  • Note: `.navigationBarTitle("")` does hide text, but awkwardly leaves spacing behind in an empty View. So one could get away with just using the modifier `.navigationBarHidden(true)` and then implement a custom back button or alternative navigation. – Sami Fouad Jan 20 '21 at 10:22
  • 2
    The problem with using navigation view and just hiding stuff is that 1) you're still in a push / pop model and if your'e never intending to go back (pop), then that view is sitting there eating memory and cycles (depending on its structure and complexity), and is never disposed. Maybe that's not a problem in the grand scheme, but if feels a little oily to me. – ChrisH Jan 27 '21 at 21:29
7

I think this is the easiest and clear way. Use fullScreenCover after UI tool.

Button(action: {
         //code
         }){
           Text("Send")
         }.fullScreenCover(isPresented: self.$model.goToOtherView, content: {
                    OtherView()
                })
bulutgonulalan
  • 316
  • 3
  • 6
7

iOS 16, SwiftUI 4

NavigationStack

If you are going to support iOS 16 and above, you can use the benefits that NavigationStack is offering.

You can see it as a stack that can push your views into. So it will be so easier to navigate a user through the app.

If this is our detailed view that we want to move here:

struct DetailView: View {
   var body: some View {
     Text("You are at detail view")
  }
}

First approach is By using the NavigationLink like before. We can move to a new view.

struct ContentView: View {
var body: some View {
    NavigationStack {
        VStack {
            NavigationLink {
                DetailView()
            } label: {
                Text("Show Detail")
            }
        }
        .navigationTitle("Navigation")
    }
   }
}

The second approach will use the value inside the NavigationLink. You can pass a value and then define .navigationDestination based on value types. as soon as user trigger that link SwiftUI will know which navigationDestination should be responsible for handling that.

struct ContentView: View {
@State var fruits: [String] = ["", "", "", ""]
var body: some View {
    NavigationStack {
        VStack(spacing: 20) {
            NavigationLink("Navigate: String Value", value: "New Page")
            
            NavigationLink("Navigate: Int Value", value: 1)
            
            ForEach(fruits, id:\.self) { fruit in
                NavigationLink(fruit, value: fruit)
            }
        }
        .navigationDestination(for: String.self) { value in
            Text("New screen")
            Text("Value is String -> \(value)")
        }
        .navigationDestination(for: Int.self) { value in
            Text("New screen")
            Text("Value is Integer -> \(value)")
        }
    }
}
}

Third approach NavigationStack shows its worth by defining a path. We can define a path and control our navigation based on the type. It will be so much easier to navigate programmatically. We can add views or remove them from the path list. It will also be so helpful when we want to navigate back to the root view by making the path list empty.

struct ContentView: View {

@State var path: [String] = []
@State var fruits: [String] = ["", "", "", ""]

var body: some View {
    NavigationStack(path: $path) {
        ZStack {
            Button("Random fruits") {
                let random = fruits.randomElement()!
                path.append(random)
            }
        }
        .navigationDestination(for: String.self) { value in
            VStack(spacing: 20) {
                Text("New screen")
                Text("Value is String -> \(value)")
                
                Button("Random fruits") {
                    let random = fruits.randomElement()!
                    path.append(random)
                }
                
                Button("Back to Root") {
                    path.removeAll()
                }
            }
        }
    }
  }
}

enter image description here

Sajjad Sarkoobi
  • 827
  • 8
  • 18
5

I think Jake's answer is the basic way to NextView.

And I think the way bellow is a simple, formal, and dynamic way to try, if you really need to hit a BUTTON. According to Paul Hudson's Video, from 10'00" to 12'00".

(should go to 12'00" to 15'00", if you want to go to different views by tapping different buttons.) (should go to 15'00" to 16'00", if you want to go to second view and go back automatically.) And more

And here is the code example.

import SwiftUI

struct ContentView: View {

    @State var areYouGoingToSecondView: Bool // Step 2

    var body: some View {
        NavigationView{ // Step 1

            VStack {

                // Step 3
                NavigationLink(destination: YourSecondView(), isActive: $areYouGoingToSecondView) { EmptyView() }


                Text("Hello World")

                Button(action: {
                    self.areYouGoingToSecondView = true // Step 4

                }) {
                    Text("Do Something (Go To Second View)")
                    .font(.largeTitle)
                    .fontWeight(.ultraLight)
                }
            }
        }
    }
}
Braver Chiang
  • 351
  • 2
  • 13
3

Here's another way to present a view WITHOUT using NavigationView. This is like UIKit's UIModalPresentationStyle.currentContext.

struct PresenterButtonView: View {
var body: some View {
    PresentationButton(Text("Tap to present"),
                       destination: Text("Hello world"))
}}
SMP
  • 1,629
  • 7
  • 15
3

The OP has been answered multiple times here but I just wanted to also demonstrate the cool aspects of SwiftUI by showing if you have view A with data that view B will also be using, you can pass data by creating a @State in view A and declaring the same variable with @Binding declaration in view B

struct ViewA : View {
    @State var myItems: [Items]
    var body: some View {
        NavigationView {
            VStack {
                NavigationButton(destination: ViewB(items: $myItems)) {
                    Text("Go To ViewB")
                }
            }
        }
    }
}
struct ViewB : View {
    @Binding var myItems: [Items]
    var body: some View {
        NavigationView {
            List{
                ForEach(myItems.identified(by: \.self)) {
                    Text($0.itemName)
                }
            }.navigationBarTitle(Text("My Items"))
        }
    }
}
nikoclicks
  • 179
  • 1
  • 8
3

Now we can use NavigationLink


File A:

struct ContentView: View {
var body: some View {
    NavigationView {
        VStack {
            Text("Hello World")
            NavigationLink(destination: secondView()) {
                Text("Hit Me!")
                    .fontWeight(.semibold)
                    .font(.title)
                    .padding()
                    .foregroundColor(.white)
                    .background(LinearGradient(gradient: Gradient(colors: [Color(.white),Color(.blue)]), startPoint: .leading, endPoint: .trailing))
                    .cornerRadius(40)
        }
      }
    }
  }
}

File B:

struct secondView: View {
var body: some View {
    VStack {
    VStack(alignment: .leading) {
        Text("Turtle Rock")
            .font(.title)
        HStack(alignment: .top) {
            Text("Joshua Tree National Park")
                .font(.subheadline)
            Spacer()
            Text("California")
                .font(.subheadline)
        }
    }
    .padding()
        Spacer()

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

You can no longer use NavigationButton. Instead you should use NavigationLink.

struct ContentView: View {
    var body: some View {
        NavigationView {
            NavigationLink(destination: DetailView()) {
                Text("Push new screen")
            }
        }
    }
}
Edward
  • 2,864
  • 2
  • 29
  • 39
1

To programmatically navigate to a link without displaying a visual element, overlay one of your views with a hidden NavigationLink.

In this sample, the UI will navigate when some code sets shouldNavigate to true:

struct ProgramaticSample {
    @State var shouldNavigate = false
    
    var body: some View {
        Text("Hello navigation")
            .overlay(NavigationLink(
                destination: DestinationScreen(),
                isActive: $shouldNavigate) {}
                .hidden())
    }
}
Edward Brey
  • 40,302
  • 20
  • 199
  • 253
0

We can use Text inside Navigation and make it like Button enter image description here http://g.recordit.co/CkIkjvikfu.gif

struct HomeContent: View {
    var body: some View {
        NavigationView{
            VStack {
                NavigationLink(
                destination: LoginContentView()) {
                    
                        
                           
                    Text("Login")
                        .font(.title)
                        .foregroundColor(Color.white)
                        .multilineTextAlignment(.center)
                        .frame(width: 300.0, height: 50.0)
                        .background(Color(UIColor.appLightBlue))
                   
                }
                
            }
        }
    }
}
Varun Naharia
  • 5,318
  • 10
  • 50
  • 84
  • 1
    I think you've missed the point of the question. 1) login shouldn't have a back button, 2) whatever was left under the login form is still there, isn't removed, isn't disposed, and 3) what happens when you push login? Surely you're going to want to go to a different screen altogether, perhaps the app's main Home Screen, which should become the new root, and not somewhere int he middle of a navigation push/pop stack. – ChrisH Jan 27 '21 at 21:33
  • Please read question again, question was to open new screen and my code is opening new screen. – Varun Naharia Jan 29 '21 at 07:21
0
var body: some View {
        
        NavigationView{
            Button(action: {
                print("Login Button click")
            }){
                NavigationLink(destination: DestinationView()) {
        
                    Text("Login")
                        .foregroundColor(.black)
                        .frame(width: 220, height: 50)
                        .background(Color.green)
                }
            }
        }
    }

struct DestinationView: View {
    var body: some View {
        Text("Hello, World!")
    }
}
Helal Khan
  • 867
  • 3
  • 10
  • 25
0

This is my approach to solving this in SwiftUI using NavigationLink and tags. I'm pretty new to programming so please be polite :)

  1. Create Custom Button to be implemented later on:

    struct CustomButton: View {
    
    @State private var selection: String? = nil
    
    var title: String
    var subtitle: String
    var tag: String
    
    var body: some View {
    
        NavigationLink(destination: TheoryView(), tag: "A", selection: $selection) {EmptyView()}
        NavigationLink(destination: PatternsView(), tag: "B", selection: $selection) { EmptyView() }
        NavigationLink(destination: Text("Online Shop"), tag: "C", selection: $selection) { EmptyView() }
        NavigationLink(destination: Text("Calendar"), tag: "D", selection: $selection) { EmptyView() }
    
        Button(action: {
            selection = tag
        }) {
            VStack(alignment: .center) {
                Text(title)
    
                    .font(.title2)
                    .fontWeight(.bold)
                    .foregroundColor(.red)
    
                Text(subtitle)
    
                    .font(.subheadline)
                    .fontWeight(.medium)
                    .foregroundColor(.white)
            }
            .fixedSize(horizontal: true, vertical: false)
            .frame(width: 200)
        } 
    }
    

    }

  2. Implement Custom Button into ContentView:

    struct ContentView: View {
    var body: some View {
        NavigationView {
            VStack {
                Group {
                    Spacer()
                    Image("logo")
                        .resizable()
                        .scaledToFit()
                        .frame(width: 180, height: 180)
    
                    Text("")
    
                    CustomButton(title: "TEORIA", subtitle: "Teoria Taekwon - DO", tag: "A")
                    CustomButton(title: "UKŁADY FORMALNE", subtitle: "Układy Formalne", tag: "B")
                    CustomButton(title: "SKLEP ONLINE", subtitle: "Nasz sklep Online", tag: "C")
                    CustomButton(title: "KALENDARZ", subtitle: "Kalendarz Imprez", tag: "D")
    
                    Spacer()
                }
               .padding(.vertical, -12)
               .buttonStyle(BlueCapsule())
    
            }
        }
    }
    

    }

Theo
  • 1
  • 2
-1

Have to create a DetailView like LandmarkDetail() and call a NavigationButton with destination as LandmarkDetail(). Now detail view was open.

For passing values to detail screen means. by sending like below code.

struct LandmarkList: View {
    var body: some View {
        NavigationView {
            List(landmarkData) { landmark in
                NavigationButton(destination: LandmarkDetail()) {
                    LandmarkRow(landmark: landmark)
                }
            }
            .navigationBarTitle(Text("Landmarks"))
        }
    }
}
Kathiresan Murugan
  • 2,783
  • 3
  • 23
  • 44