New in SwiftUI 4 & iOS 16 : NavigationStack

DevTechie Inc
Jun 24, 2022


Photo by Hello I'm Nik on Unsplash

NavigationStack is our new friend in SwiftUI 4, who will help us manage our app’s navigation better.

Introduced with iOS 16 in WWDC22, NavigationStack brings UINavigationController like functionality to the SwiftUI world.

NavigationStack is a view that displays a root view and enables us to present additional views over the root view. NavigationStack also manages stack of views over the root view so pushed views can be popped out using built-in, platform-appropriate way, like via Back button or a swipe gesture.

NavigationStack works with newly overloaded NavigationLink which takes hashable value as a param and pass it on to a newly introduced modifier called navigationDestination. Let’s take an example for this.

For our first example, we will create a NavigationStack which will have a NavigationLink to push a navigationDestination which will have a view. In this case we will have a simple Text view as the destination view.

struct ContentView: View {
    var body: some View {
        NavigationStack {
            NavigationLink("Next view please", value: "DevTechie.com")
            .navigationDestination(for: String.self) { val in
                Text("Here is the second view with value: \(val)")
                    .font(.title)
            }
        }
    }
}
Take a note on the value that was passed from NavigationLink to navigationDestination and it was displayed on the detail view.
NavigationStack’s value parameter and navigationDestination’s for parameter must be of same type. If the types differ then navigation will not work as shown in the example below. We also get a message in console log like this: [SwiftUI] A NavigationLink is presenting a value of type “String” but there is no matching navigation destination visible from the location of the link. The link cannot be activated.

struct ContentView: View {
    var body: some View {
        NavigationStack {
            NavigationLink("Next view please", value: "DevTechie.com")
            .navigationDestination(for: Int.self) { val in
                Text("Here is the second view with value: \(val)")
                    .font(.title)
            }
        }
    }
}
Types are not limited to primitive types but any type conforming to Hashable protocol can be used with NavigationStack.

Let’s create a data structure for our next example. We will start with a struct which will represents courses from DevTechie.com (check out the website for more details.)

struct DevTechieCourse: Identifiable, Hashable {
    let id = UUID()
    let name: String
}
We also need some sample data, so we will add them into this struct’s extension: (These are real courses BTW and there are more on DevTechie.com)

extension DevTechieCourse {
    static var exampleData: [DevTechieCourse] {
        return [
            .init(name: "Mastering SwiftUI"),
            .init(name: "Build Disney Plus Clone in SwiftUI"),
            .init(name: "Build Video Player App in SwiftUI"),
            .init(name: "Build Drawing App in SwiftUI")
        ]
    }
}
Now when we have our data structure ready, let’s put it all in our content view:

struct ContentView: View {
    var body: some View {
        NavigationStack {
            Text("DevTechie!")
                .font(.largeTitle)
                .foregroundColor(.primary)
            
            List(DevTechieCourse.exampleData) { course in
                NavigationLink(course.name, value: course)
            }
            .listStyle(.plain)
            .navigationDestination(for: DevTechieCourse.self) { course in
                Text(course.name)
            }
        }
        .padding()
    }
}
NavigationLink takes custom view as trailing closure as well:

struct ContentView: View {
    
    var body: some View {
        NavigationStack {
            Text("DevTechie!")
                .font(.largeTitle)
                .foregroundColor(.primary)
            
            List(DevTechieCourse.exampleData) { course in
                NavigationLink(value: course) {
                    Text(course.name)
                        .font(.title3)
                }
            }
            .listStyle(.plain)
            .navigationDestination(for: DevTechieCourse.self) { course in
                Text(course.name)
            }
        }
        .padding()
    }
}
NavigationStack and navigationDestination are capable of identifying and handling multiple types of values as well. For example, we will add a text to show our website DevTechie.com at the top of the list:

struct ContentView: View {
    
    var body: some View {
        NavigationStack {
            Text("DevTechie!")
                .font(.largeTitle)
                .foregroundColor(.primary)
            NavigationLink(value: "DevTechie.com") {
                Text("Visit our website for more content!!!")
            }
            List(DevTechieCourse.exampleData) { course in
                NavigationLink(value: course) {
                    Text(course.name)
                        .font(.title3)
                }
            }
            .listStyle(.plain)
            .navigationDestination(for: DevTechieCourse.self) { course in
                Text(course.name)
            }
            .navigationDestination(for: String.self) { value in
                Text(value)
            }
        }
        .padding()
    }
}
Notice that we have two navigationDestinations and each displays content accordingly.

By Default, a NavigationStack manages state to keep track of all the views on the stack but real power of NavigationStack comes with another new addition called NavigationPath. NavigationPath is used for programmatic navigation.

With NavigationPath we can programmatically control of the state of navigation.

We start by a declaration of NavigationPath State property and passing it as a binding parameter to the NavigationStack:

struct ContentView: View {
    @State private var path = NavigationPath()
    
    var body: some View {
        NavigationStack(path: $path) {
            Text("DevTechie!")
                .font(.largeTitle)
                .foregroundColor(.primary)
            
            List(DevTechieCourse.exampleData) { course in
                NavigationLink(value: course) {
                    Text(course.name)
                        .font(.title3)
                }
            }
            .listStyle(.plain)
            .navigationDestination(for: DevTechieCourse.self) { course in
                Text(course.name)
            }
            
        }
        .padding()
    }
}
Our navigation should still work the same:

But this addition gives us ability to programmatically navigate. How about popping to the root view controller from a deep hierarchy?

Let’s add a link to our navigationDestination so we can go few views deep:

struct ContentView: View {
    @State private var path = NavigationPath()
    
    var body: some View {
        NavigationStack(path: $path) {
            Text("DevTechie!")
                .font(.largeTitle)
                .foregroundColor(.primary)
            
            List(DevTechieCourse.exampleData) { course in
                NavigationLink(value: course) {
                    Text(course.name)
                        .font(.title3)
                }
            }
            .listStyle(.plain)
            .navigationDestination(for: DevTechieCourse.self) { course in
                Text(course.name)
                NavigationLink(DevTechieCourse.exampleData[3].name, value: DevTechieCourse.exampleData[3])
            }
            
        }
        .padding()
    }
}
This will give us following behavior:

How about adding back to root button so instead of clicking back button bunch of times, we just pop right back to the list view:

struct ContentView: View {
    @State private var path = NavigationPath()
    
    var body: some View {
        NavigationStack(path: $path) {
            Text("DevTechie!")
                .font(.largeTitle)
                .foregroundColor(.primary)
            
            List(DevTechieCourse.exampleData) { course in
                NavigationLink(value: course) {
                    Text(course.name)
                        .font(.title3)
                }
            }
            .listStyle(.plain)
            .navigationDestination(for: DevTechieCourse.self) { course in
                Text(course.name)
                NavigationLink(DevTechieCourse.exampleData[3].name, value: DevTechieCourse.exampleData[3])
                Button("Back to root") {
                    path = NavigationPath()
                }
            }
            
        }
        .padding()
    }
}
Here we are simply resetting path variable by reinitializing it, which will push our view back to the root view. Notice that the root view is never removed from the navigation hierarchy.

There is another use-case I want to cover with NavigationPath. NavigationPath can be a collection of Hashable type. For example, it can be a collection of DevTechieCourse in our case.

struct ContentView: View {
    @State private var path = [DevTechieCourse]()
    
    var body: some View {
        NavigationStack(path: $path) {
            Text("DevTechie!")
                .font(.largeTitle)
                .foregroundColor(.primary)
            
            List(DevTechieCourse.exampleData) { course in
                NavigationLink(value: course) {
                    Text(course.name)
                        .font(.title3)
                }
            }
            .listStyle(.plain)
            .navigationDestination(for: DevTechieCourse.self) { course in
                Text(course.name)
                NavigationLink(DevTechieCourse.exampleData[3].name, value: DevTechieCourse.exampleData[3])
            }
            
        }
        .padding()
    }
}
Our behavior should still be the same:

But with this new change, we can place deep links inside our SwiftUI views. Let’s update our example to add two paths, Mastering SwiftUI and DisneyPlus clone. We will add both as if we are receiving these values from an external source and then launch the view:

struct ContentView: View {
    @State private var path: [DevTechieCourse] = [DevTechieCourse.exampleData[0], DevTechieCourse.exampleData[1]]
    
    var body: some View {
        NavigationStack(path: $path) {
            Text("DevTechie!")
                .font(.largeTitle)
                .foregroundColor(.primary)
            
            List(DevTechieCourse.exampleData) { course in
                NavigationLink(value: course) {
                    Text(course.name)
                        .font(.title3)
                }
            }
            .listStyle(.plain)
            .navigationDestination(for: DevTechieCourse.self) { course in
                Text(course.name)
                NavigationLink(DevTechieCourse.exampleData[3].name, value: DevTechieCourse.exampleData[3])
            }
            
        }
        .padding()
    }
}
Upon launching our app now, we will find ourselves at the 2nd detail view and clicking back button twice will bring us to the root view.

With that we have reached the end of this article. Thank you once again for reading. Subscribe to our weekly newsletter at https://www.devtechie.com