Skip to main content

How to use programmatic navigation in SwiftUI

About 4 minSwiftSwiftUIArticle(s)bloghackingwithswift.comcrashcourseswiftswiftuixcodeappstore

How to use programmatic navigation in SwiftUI 관련

SwiftUI by Example

Back to Home

How to use programmatic navigation in SwiftUI | SwiftUI by Example

How to use programmatic navigation in SwiftUI

Updated for Xcode 15

Updated for iOS 16

We can use SwiftUI to programmatically push a new view onto a NavigationStack using NavigationLink, meaning that we can trigger the navigation when we're ready rather than just when the user tapped a button or list row.

Important

There are two approaches to programmatic navigation: the newer, more powerful option available from iOS 16 and later, or the older, simpler option available available in earlier releases. Apple has formally deprecated the older API, so you should move away as soon as you're able. In the meantime, if you need to support iOS 15 and earlier you should see below.

From iOS 16 and later, we can pass an array of Hashable data directly to the NavigationStack to control which data is currently on the stack. For example, this tracks numbers being presented, and starts by immediately pushing 1, 4, and 8 onto the stack:

struct ContentView: View {
    @State private var presentedNumbers = [1, 4, 8]

    var body: some View {
        NavigationStack(path: $presentedNumbers) {
            List(1..<50) { i in
                NavigationLink(value: i) {
                    Label("Row \(i)", systemImage: "\(i).circle")
                }
            }
            .navigationDestination(for: Int.self) { i in
                Text("Detail \(i)")
            }
            .navigationTitle("Navigation")
        }
    }
}

Download this as an Xcode projectopen in new window

When that code runs, the user will see “Detail 8”, and can tap back to “Detail 4”, then “Detail 1”, until eventually reaching the list of numbers.

This approach is powerful, because we can at any point modify the navigation stack to push a custom view on there – it's a simple array, so we can append items, or insert them, remove them, or whatever else we need. In this code sample the path array starts empty, then gets added to over time by using the List or clicking buttons:

struct ContentView: View {
    @State private var presentedNumbers = [Int]()

    var body: some View {
        NavigationStack(path: $presentedNumbers) {
            List(1..<50) { i in
                NavigationLink(value: i) {
                    Label("Row \(i)", systemImage: "\(i).circle")
                }
            }
            .navigationDestination(for: Int.self) { i in
                VStack {
                    Text("Detail \(i)")

                    Button("Go to Next") {
                        presentedNumbers.append(i + 1)
                    }
                }
            }
            .navigationTitle("Navigation")
        }
    }
}

Download this as an Xcode projectopen in new window

Tips

This means you can restore the full state of an app – including its full navigation state – by serializing your navigation path.

Using a simple array for your navigation path is fine if you're only pushing one data type onto your stack, but if you need heterogeneous data to use a special type-erased wrapper called NavigationPath. This can work with any hashable data, so you could add a few strings, a few integers, a few custom structs, etc, and as long as they all conform to Hashable you're okay.

For example, this code lets the user navigate to any row in a list using a string navigation destination, but also has a button to insert a number into the path:

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

    var body: some View {
        NavigationStack(path: $navPath) {
            Button("Jump to random") {
                navPath.append(Int.random(in: 1..<50))
            }

            List(1..<50) { i in
                NavigationLink(value: "Row \(i)") {
                    Label("Row \(i)", systemImage: "\(i).circle")
                }
            }
            .navigationDestination(for: Int.self) { i in
                Text("Int Detail \(i)")
            }
            .navigationDestination(for: String.self) { i in
                Text("String Detail \(i)")
            }
            .navigationTitle("Navigation")
        }
    }
}

Download this as an Xcode projectopen in new window

You can adjust your path however you want – we appended a single value there, but you could append multiple values at once if needed, and something like the old UIKit “pop to root view controller” becomes just as simple as clearing everything from your path – something like navPath.removeLast(navPath.count) should do the trick.


Supporting iOS 15 and earlier

There are two ways of doing this, both of which rely on initializers for NavigationLink. The first is binding the NavigationLink to a Boolean state – when that Boolean becomes true the navigation will happen immediately, and when it becomes false again the new view will be dismissed.

SwiftUI does require that we pass some sort of view to NavigationLink even when doing programmatic navigation. You'll probably want to use EmptyView to show nothing at all, for example here's a complete example of programmatic navigation, where I'm toggling the Boolean on a button press:

struct ContentView: View {
    @State private var isShowingDetailView = false

    var body: some View {
        NavigationView {
            VStack {
                NavigationLink(destination: Text("Second View"), isActive: $isShowingDetailView) { EmptyView() }

                Button("Tap to show detail") {
                    isShowingDetailView = true
                }
            }
            .navigationTitle("Navigation")
        }
    }
}

Download this as an Xcode projectopen in new window

The advantage to this approach over a simple NavigationLink is that our button can do any amount of other work before triggering the programmatic navigation – maybe you want to save some data, or authenticate the user, etc.

If you have several possible destinations, you can bind more than one NavigationLink to some selection state, giving each one a unique tag. When you update your selection state to match one of those tags will cause the appropriate NavigationLink to activate, which gives you multi-destination programmatic navigation without having lots of Booleans.

For example, this navigates to one of two destination text views depending on the value of a selection property:

struct ContentView: View {
    @State private var selection: String? = nil

    var body: some View {
        NavigationView {
            VStack {
                NavigationLink(destination: Text("View A"), tag: "A", selection: $selection) { EmptyView() }
                NavigationLink(destination: Text("View B"), tag: "B", selection: $selection) { EmptyView() }

                Button("Tap to show A") {
                    selection = "A"
                }

                Button("Tap to show B") {
                    selection = "B"
                }
            }
            .navigationTitle("Navigation")
        }
    }
}

Download this as an Xcode projectopen in new window

Similar solutions…
SwiftUI tips and tricks | SwiftUI by Example

SwiftUI tips and tricks
How to embed a view in a navigation view | SwiftUI by Example

How to embed a view in a navigation view
Introduction to navigation | SwiftUI by Example

Introduction to navigation
What's the difference between @ObservedObject, @State, and @EnvironmentObject? | SwiftUI by Example

What's the difference between @ObservedObject, @State, and @EnvironmentObject?
How to add bar items to a navigation view | SwiftUI by Example

How to add bar items to a navigation view

이찬희 (MarkiiimarK)
Never Stop Learning.