I'm trying to use NavigationStack with Views in SwiftUI, but I'm having trouble understanding its functionality. I have gone through several tutorials, but I still don't understand the purpose of the value
parameter and why all the tutorials use ForEach
to create subviews instead of linking actual views.
In my case, I’m just trying to make the following code work and go back to the ContentView() when I press the “Go to ContentView” button, but nothing happens and the rootPath.count remains at 0:
-----------------ContentView-----------------
struct ContentView: View {
@State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
Image(systemName: "figure.fall")
.imageScale(.large) .foregroundStyle(.tint)
NavigationLink("Go to FirstView") {
FirstView(rootPath: $path)
}
}
.padding()
}
}
------------------FirstView------------------
struct FirstView: View {
@Binding var rootPath: NavigationPath
var body: some View {
NavigationLink("Go to SecondView") {
SecondView(rootPath: $rootPath)
}
}
}
-----------------SecondView------------------
struct SecondView: View {
@Binding var rootPath: NavigationPath
var body: some View {
NavigationLink("Go to ThirdView") {
ThirdView(rootPath: $rootPath)
}
}
}
------------------ThirdView------------------
struct ThirdView: View {
@Binding var rootPath: NavigationPath
var body: some View {
Text("RootPath: \(rootPath.count)")
Button(action: {
rootPath = NavigationPath()
}, label: {
Text("Go to ContentView")
})
}
}
enum Destination: Hashable { case first, second, third }
You associate the value to the view when navigationDestination
gets called. The root view doesn't need a value.
NavigationPath
type (it's only for cases where your view value types can be different), instead almost always just an array will do. You update the path by NavigationLink or alternatively just assigning/appending/removing something to it directly. The path is on top of the root, so making it empty gets you back all the way.var path: [Destination] = []`
Here's your code modified with the points above, it might clarify things a bit.
class PathState: ObservableObject {
enum Destination: String, Hashable {
case first,second,third
}
@Published var path: [Destination] = []
}
//-----------------ContentView-----------------
struct ContentView: View {
@StateObject var pathState = PathState()
var body: some View {
NavigationStack(path: $pathState.path) {
Image(systemName: "figure.fall")
.imageScale(.large)
.foregroundStyle(.tint)
.navigationDestination(for: PathState.Destination.self) { destination in
switch destination {
case .first:
FirstView()
case .second:
SecondView()
case .third:
ThirdView()
}
}
NavigationLink("Go to FirstView", value: PathState.Destination.first)
}
.padding()
.overlay(alignment: .top) {
Text("Navigation Path: \(pathState.path.map(\.rawValue).debugDescription)")
}
.environmentObject(pathState)
}
}
//------------------FirstView------------------
struct FirstView: View {
var body: some View {
NavigationLink("Go to SecondView", value: PathState.Destination.second)
}
}
//-----------------SecondView------------------
struct SecondView: View {
var body: some View {
NavigationLink("Go to ThirdView", value: PathState.Destination.third)
}
}
//------------------ThirdView------------------
struct ThirdView: View {
@EnvironmentObject var pathState: PathState
var body: some View {
Text("RootPath: \(pathState.path.count)")
Button(action: {
pathState.path = [] // take everything off the navigation stack
}, label: {
Text("Go to ContentView")
})
}
}
More info about navigationDestination:
As you make new navigation screens add them to the enum. If you want to define them all up front, you can just add a default to the switch to catch the cases where the view hasn't been built yet, like default: Text("Under Construction")
.
Now, you don't need to use a custom enum for the path value or make a PathState
object at all, that is just one way to organize things and makes sense for this example. The navigationDestination just needs to return some view
for a given navigation value
that is being pushed. It can always be the same general view. Like:
.navigationDestination { screenNumber in
ScreenView(number: screenNumber)
}
struct ScreenView: View {
let number: Int
var body: some View { Text("This is screen \(number)") }
}
or just ignore the value and use some other logic.
.navigationDestination { _ in
if Calendar.autoupdating.isDateInWeekend(.now) {
return WeekendView()
} else {
return WeekdayView()
}
}