WWDC23 SALE: Save 50% on all my Swift books and bundles! >>

How to use programmatic navigation in SwiftUI

Paul Hudson    @twostraws   

Updated for Xcode 14.2

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 project

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 project

Tip: 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 project

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 project

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 project

Save 50% in my WWDC23 sale.

SAVE 50% To celebrate WWDC23, all our books and bundles are half price, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.

Save 50% on all our books and bundles!

Similar solutions…

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let us know!

Average rating: 4.8/5

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.