This post will go over how to use NavigationStack and NavigationLink, which are the two main components for managing the navigation stack and navigating between views in an app.

About six months ago, Apple announced that the NavigationView will be deprecated in iOS 16.2. NavigationView was the primary method of managing an app’s various levels, from the main menu to the deeper levels. It enabled developers to create complex, hierarchical apps quickly and easily. Its straightforward design, which was consistent across Apple’s platforms, made it a popular choice among developers. However, the NavigationView will be deprecated in iOS 16.2. As a result, developers will need to consider alternative solutions for managing the various levels of their apps. Apple has provided some guidance on the best course of action.

Although Apple has not deprecated the NavigationLink component (which allows us to push from a new perspective), it has deprecated some of its methods as of iOS 16.0. It’s the case of methods like init( :isActive:destination:) or init( :tag:selection:destination:), which allow you to check if a link is active (isActive) or pass data to a new window (tag).

Before iOS 16.2

This is an example of how NavigationView was used before its deprecation:

struct ContentView: View {
    let items = [Item(name: "Item 1"), Item(name: "Item 2"), Item(name: "Item 3")]

    var body: some View {
        NavigationView {
            List(items) { item in
                NavigationLink(destination: DetailView(item: item)) {
                    Text(item.name)
                }
            }
            .navigationBarTitle("Home")
            .navigationBarItems(leading:
                Button(action: {
                    print("Leading button tapped")
                }) {
                    Image(systemName: "list.bullet")
                }, trailing:
                Button(action: {
                    print("Trailing button tapped")
                }) {
                    Image(systemName: "heart")
                }
            )
            .toolbar(.hidden)
            .navigationBarBackButtonHidden(true)
        }
    }
}

struct DetailView: View {
    var item: Item

    var body: some View {
        VStack {
            Text("Details for: \(item.name)")
            NavigationLink(destination: ContentView(), isActive: .constant(false), label: {
                Text("Go back to Home")
            })
        }
        .navigationBarTitle("Detail View")
    }
}

struct Item: Identifiable {
    let id = UUID()
    let name: String
}

From iOS 16.2: NavigationStack

On iOS 16.2, Apple changed NavigationView by NavigationStack. According to Apple:

A view that displays a root view and enables you to present additional views over the root view.

That is, it is a root view to which more views can be added on top (push).

Let’s see an example:

struct ContentView: View {
    let items = [Item(name: "Item 1"), Item(name: "Item 2"), Item(name: "Item 3")]
    
    var body: some View {
        NavigationStack {
            List(items) { item in
                NavigationLink(destination: DetailView(item: item)) {
                    Text(item.name)
                }
            }
            .navigationTitle("Home")
        }
    }
}

struct DetailView: View {
    var item: Item
    
    var body: some View {
        VStack {
            Text("Details for: \(item.name)")
        }
        .navigationTitle("Detail View")
    }
}

It’s the same example as the old NavigationView example. We only have changed NavigationView with NavigationStack, but it has the same behaviour: shows some links and, opn tap on ones of those links (NavigationLink), the DetailedView is pushed to the stack.

Data-driven navigation

With the introduction of the NavigationStack, Apple has also introduced a data-driven navigation model. What does this mean, since it has added a new modifier (navigationDestination) that relates the destination to which you want to navigate with a specific data type.

Let’s modify the previous example in order to use navigationDestination:

struct ContentView: View {
    let items = [Item(name: "Item 1"), Item(name: "Item 2"), Item(name: "Item 3")]
    
    var body: some View {
        NavigationStack {
            List(items) { item in
                NavigationLink(item.name, value: item)
            }
            .navigationDestination(for: Item.self) { item in
                DetailView(item: item)
            }
            .navigationTitle("Home")
        }
    }
}

struct DetailView: View {
    var item: Item
    
    var body: some View {
        VStack {
            Text("Details for: \(item.name)")
        }
        .navigationTitle("Detail View")
    }
}

struct Item: Identifiable, Hashable {
    let id = UUID()
    let name: String
}

Now, in the NavigationLink we are not passing a view, but data (in this case, of the Item type, although we could add more types). Next, in the navigationDestination(for:) modifier we set the navigation. This is where it is possible to add different paths depending on the type passed to it. For example:

struct ContentView: View {
    let items = [Item(name: "Item 1"), Item(name: "Item 2"), Item(name: "Item 3")]
    let boxes = [Box(name: "Box 1"), Box(name: "Box 2"), Box(name: "Box 3")]
    
    var body: some View {
        NavigationStack {
            List {
                ForEach(boxes) { box in
                    NavigationLink(box.name, value: box)
                }
                ForEach(items) { item in
                    NavigationLink(item.name, value: item)
                }
            }
            .navigationDestination(for: Box.self) { box in
                BoxView(box: box)
            }
            .navigationDestination(for: Item.self) { item in
                ItemView(item: item)
            }
            .navigationTitle("Home")
        }
    }
}

struct ItemView: View {
    var item: Item
    
    var body: some View {
        VStack {
            Text("Details for: \(item.name)")
        }
        .navigationTitle("Item View")
    }
}

struct BoxView: View {
    var box: Box
    
    var body: some View {
        VStack {
            Text("Details for: \(box.name)")
        }
        .navigationTitle("Box View")
    }
}

struct Item: Identifiable, Hashable {
    let id = UUID()
    let name: String
}

struct Box: Identifiable, Hashable {
    let id = UUID()
    let name: String
}

Where if the item passed is of type Item, it will show a DetailedView, while for a type Box it will show a BoxView.

Conclusion

NavigationStack is a game changer for Navigation in SwiftUI. allowing to upgrade our apps.