Timo
Timo

Reputation: 51

Are multiple NavigationStacks allowed in SwiftUI for nested navigation scenarios while utilizing .toolbar modifier?

I am working on a SwiftUI application where I have a list of items on the main screen. When an item is selected, it navigates to a detailed view of that item. On this detailed view, there are other clickable elements that should navigate to other views. Initially, I thought of wrapping each view in a NavigationStack to manage the navigation and take advantage of the possibility to use .toolbar. However, I do expect that using multiple NavigationStacks may not be a good practice and could lead to a confusing navigation hierarchy and potential performance issues.

Here's a simplified version of my current structure:

struct MainView: View {
    var body: some View {
        NavigationStack {
            List(items) { item in
                NavigationLink(destination: DetailView(item: item)) {
                    Text(item.name)
                }
            }
            .toolbar {
                // Toolbar items for MainView
            }
        }
    }
}

struct DetailView: View {
    var item: Item
    
    var body: some View {
        NavigationStack {
            VStack {
                Text(item.description)
                NavigationLink(destination: AnotherView()) {
                    Text("Go Further")
                }
            }
            .toolbar {
                // Toolbar items for DetailView
            }
        }
    }
}

Is there a recommended way to manage multi-level navigation in SwiftUI, utilize the .toolbar modifier, and adhere to good practices? Any examples or resources on this would be greatly appreciated.

I have come across alternatives like using a single NavigationStack or TabView, but I am unsure how to apply these to maintain a clean, navigable structure, especially when the navigation hierarchy becomes more complex, while still being able to utilize the .toolbar modifier effectively.

Upvotes: 5

Views: 2409

Answers (1)

Marceli Wac
Marceli Wac

Reputation: 429

You can provide different .toolbar modifiers for each of the views (or nested views) without having to duplicate the NavigationStack. All you have to do is append the modifier to the outermost view in the body. Here is an example:

Example

//
//  Created by Marceli Wac on 12/05/2024.
//

import SwiftUI

// Example data struct
struct Car: Identifiable, Hashable {
    var id: String { name }
    let name: String
    let wheels: Int
}

// Example "detail" view for a data struct
struct CarView: View {
    let car: Car
    
    var body: some View {
        VStack {
            Text("\(car.name) has \(car.wheels) wheels.")
        }
        .navigationTitle("Car View (\(car.name))")
        // Toolbar specific to a different view
        .toolbar {
            ToolbarItem(placement: .topBarTrailing, content: {
                Button (action: {
                    // do something
                }, label: {
                    Image(systemName: "pin")
                    Text("Do something")
                })
            })
        }
    }
}

// Primary view
struct ContentView: View {
    @State private var navigationPath = NavigationPath()
    let cars: [Car] = [
        .init(name: "Ferrari", wheels: 4),
        .init(name: "Jeep", wheels: 6),
        .init(name: "Tesla", wheels: 4),
    ]
    
    var body: some View {
        NavigationStack(path: $navigationPath) {
            List (cars) { car in
                HStack {
                    Image(systemName: "car")
                    Text(car.name)
                    Spacer()
                }
                .onTapGesture {
                    navigationPath.append(car)
                }
            }
            .navigationTitle("Content View")
            .navigationDestination(for: Car.self, destination: { car in
                CarView(car: car)
            })
            .toolbar {
                ToolbarItem(placement: .topBarLeading, content: {
                    Text("You're at home!")
                })
            }
        }
        
    }
}

// Preview
#Preview {
    ContentView()
}

You can also nest the .navigationDestination modifiers inside other views, but their types may need to be unique globally. For example, you can add a destination for Int to the CarView, but if you add another one in the ContentView, the ContentView one will be the one that is always used.

// ...
struct CarView: View {
    let car: Car
    
    var body: some View {
        VStack {
            Text("\(car.name) has \(car.wheels) wheels.")
            // Navigate to `Int`
            NavigationLink("View wheel count", value: car.wheels)
        }
        // Handle `Int` view
        .navigationDestination(for: Int.self, destination: { wheelCount in
            Text("Wheels: \(wheelCount)")
        })
    }
}
// ...

Example with better navigation

It's worth noting that you might run into another issue where you need to access the navigation path from one of the child views. A great solution to that problem is providing a router class that exposes the navigation path passed to the original NavigationStack (in the example below it's the NavigationState class):

//
//  Created by Marceli Wac on 12/05/2024.
//

import SwiftUI

// Example data struct
struct Car: Identifiable, Hashable {
    var id: String { name }
    let name: String
    let wheels: Int
}

// Example "detail" view for a data struct
struct CarView: View {
    let car: Car
    @Environment(NavigationState.self) var navigationState: NavigationState
    
    var body: some View {
        VStack {
            Text("\(car.name) has \(car.wheels) wheels.")
            ListOfCars()
        }
        .navigationTitle("Car View (\(car.name))")
        // Toolbar specific to a different view
        .toolbar {
            ToolbarItem(placement: .topBarTrailing, content: {
                Button (action: {
                    navigationState.empty()
                }, label: {
                    Image(systemName: "pin")
                    Text("Pop stack")
                })
            })
        }
    }
}

// Utility view to allow for building up the navigation stack and demonstrating different toolbars
struct ListOfCars: View {
    @Environment(NavigationState.self) private var navigationState: NavigationState
    
    let cars: [Car] = [
        .init(name: "Ferrari", wheels: 4),
        .init(name: "Jeep", wheels: 6),
        .init(name: "Tesla", wheels: 4),
    ]
    
    var body: some View {
        List (cars) { car in
            HStack {
                Image(systemName: "car")
                Text(car.name)
                Spacer()
            }
            .onTapGesture {
                navigationState.push(car)
            }
        }
    }
}

// Router class that handles navigation. Note that it is made observable and provided as env. object
// to allow child views to access the navigation stack
@Observable class NavigationState {
    var path: NavigationPath = NavigationPath()
    
    func push<V>(_ element: V) -> Void where V:Hashable {
        path.append(element)
    }
    
    func pop() -> Void {
        path.removeLast()
    }
    
    func empty() -> Void {
        path.removeLast(path.count)
    }
}

// Primary view
struct ContentView: View {
    @State private var navigationState = NavigationState()
    
    var body: some View {
        NavigationStack(path: $navigationState.path) {
            ListOfCars()
            .navigationTitle("Content View")
            .navigationDestination(for: Car.self, destination: { car in
                CarView(car: car)
            })
            .toolbar {
                ToolbarItem(placement: .topBarLeading, content: {
                    Text("You're at home!")
                })
            }
        }.environment(navigationState)
        
    }
}

// Preview
#Preview {
    ContentView()
}

You don't have to use the @Observable macro if you need to support iOS < 16. Simply switch to the older counterparts (ObservableObject, StateObject, EnvironmentObject and add the required @Published modifiers) like in the official documentation.

Upvotes: 0

Related Questions