SwiftUI · · 5 min read

Working with the new NavigationStack in SwiftUI

Working with the new NavigationStack in SwiftUI

In iOS development, navigation view is definitely one of the most commonly used components. When SwiftUI was first released, it came with a view called NavigationView for developers to build navigation-based user interfaces. With the release of iOS 16, Apple has deprecated the old navigation view and introduced a new view known as NavigationStack to present a stack of views. Most importantly, developers can make use of this new view to build data driven navigation.

The Old Way of Navigation Views

Prior to iOS 16, you create a navigation interface using NavigationView and NavigationLink like this:

NavigationView {
    NavigationLink {
        Text("Destination")
    } label: {
        Text("Tap me")
    }
}

This creates a basic navigation based interface with a Tap me button. When tapped, the app navigates one level down to display the destination view.

swiftui-navigationstack-ios16

Working with NavigationStack

Starting from iOS 16, you replace NavigationView with the new NavigationStack. You can keep the NavigationLink intact and achieve the same result.

NavigationStack {
    NavigationLink {
        Text("Destination")
    } label: {
        Text("Tap me")
    }
}

The same piece of the code can also be written like this:

NavigationStack {
    NavigationLink("Tap me") {
        Text("Destination")
    }
}

We usually use navigation views to build a master-detail flow for a list of data items. Here is an example:

struct ContentView: View {
    private var bgColors: [Color] = [ .indigo, .yellow, .green, .orange, .brown ]

    var body: some View {

        NavigationStack {
            List(bgColors, id: \.self) { bgColor in
                NavigationLink {
                    bgColor
                        .frame(maxWidth: .infinity, maxHeight: .infinity)
                } label: {
                    Text(bgColor.description)
                }

            }
            .listStyle(.plain)

            .navigationTitle("Color")
        }

    }
}

This creates a navigation view to display a list of color items. When an item is selected, the app navigates to the detail view and shows the color view.

swiftui-navigationstack-demo

Value-based Navigation Links

NavigationStack introduces a new modifier called navigationDestination that associates a destination view with a presented data type. The same piece of code in the previous section can be rewritten like this:

NavigationStack {
    List(bgColors, id: \.self) { bgColor in

        NavigationLink(value: bgColor) {
            Text(bgColor.description)
        }

    }
    .listStyle(.plain)

    .navigationDestination(for: Color.self) { color in
        color
            .frame(maxWidth: .infinity, maxHeight: .infinity)
    }

    .navigationTitle("Color")
}

You still use NavigationLinks to present the list of data and implement the navigation feature. What’s difference is that each NavigationLink associates with a value. On top of that, we added the new navigationDestination modifier to capture the value change. When a user selects a particular link, the navigationDestination modifier presents the corresponding destination view for navigation links that present data of type Color.

If you test the app in the preview, it works exactly the same as before. However, the internal implementation already makes use of the new navigationDestination modifier.

Multiple Navigation Destination Modifiers

You are allowed to define more than one navigationDestination modifier for handling different types of the navigation links. In the previous example, we had a single navigationDestination modifier for the Color type. Let’s say, we have another set of navigation links for the String type like this:

List(systemImages, id: \.self) { systemImage in

    NavigationLink(value: systemImage) {
        Text(systemImage.description)
    }

}
.listStyle(.plain)

The systemImages variable stores an array of the system image names.

private var systemImages: [String] = [ "trash", "cloud", "bolt" ]

In this case, we have two types of navigation links. One is for the Color type, the other is the String type. To handle the navigation of the String type, we can embed another navigationDestination modifier to the stack like this:

.navigationDestination(for: String.self) { systemImage in
    Image(systemName: systemImage)
        .font(.system(size: 100.0))
}

Now if the user taps one of the system image names, it navigates to another view that displays the system image.

swiftui-navigation-destination

Working with Navigation States

Unlike the old NavigationView, the new NavigationStack allows you to easily keep track of the navigation state. The NavigationStack view has another initialization method that takes in a path parameter, which is a binding to the navigation state for the stack:

init(
    path: Binding<Data>,
    root: () -> Root
) where Data : MutableCollection, Data : RandomAccessCollection, Data : RangeReplaceableCollection, Data.Element : Hashable

If you want to store or manage the navigation state, you can create a state variable. Here is a code sample:

struct ContentView: View {
    private var bgColors: [Color] = [ .indigo, .yellow, .green, .orange, .brown ]

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

    var body: some View {

        NavigationStack(path: $path) {
            List(bgColors, id: \.self) { bgColor in

                NavigationLink(value: bgColor) {
                    Text(bgColor.description)
                }

            }
            .listStyle(.plain)

            .navigationDestination(for: Color.self) { color in
                VStack {
                    Text("\(path.count), \(path.description)")
                        .font(.headline)

                    HStack {
                        ForEach(path, id: \.self) { color in
                            color
                                .frame(maxWidth: .infinity, maxHeight: .infinity)
                        }

                    }

                    List(bgColors, id: \.self) { bgColor in

                        NavigationLink(value: bgColor) {
                            Text(bgColor.description)
                        }

                    }
                    .listStyle(.plain)

                }
            }

            .navigationTitle("Color")

        }

    }
}

The code is similar to the previous example. We added a state variable named path, which is an array of Color, to store the navigation state. During the initialization of NavigationStack, we pass its binding for managing the stack. The value of the path variable will be automatically updated when the navigation stack’s state changes.

I made a minor change for the navigation destination. It displays the user’s selected colors and shows another list of colors for further selection.

swiftui-navigation-link

In the code above, we have this line of code to display the path content:

Text("\(path.count), \(path.description)")

The count property gives you the number of levels of the stack, while the description presents the current color. Say, for example, you first select the color indigo and then further selects yellow. The value of count is 2, which means the navigation stack has two levels.

With this path variable, you can programmatically control the navigation of the stack. Let’s say, we can add a button for users to jump directly to the root level of the stack. Here is the sample code:

Button {
    path = .init()
} label: {
    Text("Back to Main")
}
.buttonStyle(.borderedProminent)
.controlSize(.large)

By resetting the value of the path variable, we can instruct the navigation stack to go back to the root level.

As you may already aware, we can manipulate the value of the path variable to control the state of the navigation stack. For example, when the ContentView appears, the app can automatically navigate down three levels by adding three colors to the path variable like this:

NavigationStack(path: $path) {
  .
  .
  .
}
.onAppear {
    path.append(.indigo)
    path.append(.yellow)
    path.append(.green)
}

When you launch the app, it automatically navigates down three levels. This is how you can control the navigation state programmatically and a great way to handle deep linking.

swiftui-navigationstack-demo-2

Summary

The new NavigationStack, introduced in iOS 16, allows developers to easily build data-driven navigation UI. If your app doesn’t need to support older versions of iOS, you can take advantage of this new component to handle deep linking and complex user flows.

Read next