Reputation: 51
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
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:
//
// 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)")
})
}
}
// ...
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