SwiftUI Navigation with NavigationStack

17 August 2025

With NavigationView deprecated, the recommended navigation approach in SwiftUI is to use NavigationStack. In my opinion, this value-based style of navigation is simpler and more flexible.

NavigationStack observes one of the following collection types:

  • Array of objects
  • NavigationPath (can hold objects of various types, behaves similar to an array)

What’s great about this is you can simply manipulate the array or NavigationPath to achieve the stack you’d like. It allows you to load a navigation stack pre-pushed down into a particular view, or pop to the root just by removing all the views from the array or NavigationPath. NavigationLink will also work as a way to push the view to the stack.

navigationDestination(for:destination:) is used to associate the view to push with the object in either the array or NavigationPath.


struct City: Identifiable, Hashable {
    let id: Int
    let name: String
}

struct CitiesView: View {
    let cities = [
        City(id: 1, name: "Paris"),
        City(id: 2, name: "Tokyo"),
        City(id: 3, name: "New York")
    ]

    @State private var path: [City] = []

    var body: some View {
        NavigationStack(path: $path) {
            List(cities) { city in
                NavigationLink(value: city) {
                    Text(city.name)
                }
            }
            .navigationTitle("Cities")
            .navigationDestination(for: City.self) { city in
                VStack {
                    Text("Welcome to \(city.name)!")
                    Button("Back to root") {
                        path.removeAll()
                    }
                }
            }
        }
    }
}

The above example shows the use of a path array. The array is bound to the NavigationStack, with navigation to views being triggered by a NavigationLink. You could manually append a City to the path instead of using a NavigationLink, and the NavigationStack would automatically push to that view.


struct Book: Hashable {
    let id: Int
    let title: String
}

struct Author: Hashable {
    let id: Int
    let name: String
}

struct BooksView: View {
    @State private var path = NavigationPath()
    var body: some View {
        NavigationStack(path: $path) {
            Text("Root")
                .navigationDestination(for: Book.self) { book in
                    Text("Book: \(book.title)")
                }
                .navigationDestination(for: Author.self) { author in
                    Text("Author: \(author.name)")
                }
        }
        .onAppear {
            path.append(Book(id: 1, title: "Dune"))
            path.append(Author(id: 10, name: "Frank Herbert"))
        }
    }
}

This example uses a NavigationPath instead of an array. When the view appears, it will already be pushed into the author level of the navigation stack. This was achieved by adding both the book and author to the NavigationPath, with author being the last item in it. The user can then tap Back, popping the author from the path and from view. This will take the user up a level to the book view.