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.