Atelier Clockwork

Navigation Refined

Testing out some of the new toys

On the long list of interesting new things to check out from WWDC this year, the first one that I wanted to take a look at was the new NavigationStack, as the limitations in the previous NavigationView led to my previous project using UIKit to back navigation in a SwiftUI first project.

After a couple minutes playing around with the docs, I came up with:

A root view:

struct NewNavigationView: View {
    private let numbers = [1, 2, 3]
    private let strings = ["a", "b", "c"]
    @State private var navigationPath = NavigationPath()

    var body: some View {
        NavigationStack(path: $navigationPath) {
            List {
                Section("Numbers") {
                    ForEach(numbers, id: \.self) {
                        NavigationLink("\($0)", value: $0)
                    }
                }
                Section("Strings") {
                    ForEach(strings, id: \.self) {
                        NavigationLink($0, value: $0)
                    }
                }
            }
            .navigationDestination(for: Int.self) { int in
                NumberView(selection: int, navigationPath: $navigationPath)
            }
            .navigationDestination(for: String.self) { string in
                StringView(selection: string, navigationPath: $navigationPath)
            }
            .navigationTitle("Root")
        }
    }
}

A view that displays details for a number:

struct NumberView: View{
    private let numbers = [1, 2, 3, 4, 5]
    let selection: Int
    @Binding var navigationPath: NavigationPath

    var body: some View {
        List {
            Section("Numbers") {
                ForEach(numbers, id: \.self) {
                    NavigationLink("\($0)", value: $0)
                }
            }
            Button("Pop to root") {
                navigationPath = NavigationPath()
            }
        }
        .navigationTitle("Detail \(selection)")
    }
}

And a view that displays details for a string:

struct StringView: View {
    private let strings = ["a", "b", "c", "d", "e"]
    let selection: String
    @Binding var navigationPath: NavigationPath

    var body: some View {
        List {
            Section("Strings") {
                ForEach(strings, id: \.self) {
                    NavigationLink("\($0)", value: $0)
                }
            }
            Button("Pop to root") {
                navigationPath = NavigationPath()
            }
        }
        .navigationTitle("Detail \(selection)")
    }
}

With just this code, I could:

  • Keep pushing new views to the stack from the inner navigation links
  • Pop to the root of the stack from within any view

It's particularly nice that you only need to add the navigationDestination modifiers at the top level of the navigation hierarchy and any child views that have a navigation link will find that destination and use it to push onto the stack.

The only caveat that I ran into in my experimenting is that if you add a second navigationDestination for the same type that another destination has claimed on a child node inside the NavigationStack it's undefined behavior and so things break in interesting ways.