Reputation: 85
I am trying to integrate NavigationStack
in my SwiftUI app.
I have four views: CealUIApp
, OnBoardingView
, UserTypeView
and RegisterView
.
I want to navigate from OnBoardingView
to UserTypeView
when user presses a button in OnBoardingView
.
And, navigate from UserTypeView
to RegisterView
when user presses a button in UserTypeView
Below is my code for CealUIApp
@main
struct CealUIApp: App {
@State private var path = [String]()
var body: some Scene {
WindowGroup {
NavigationStack(path: $path){
OnBoardingView(path: $path)
}
}
}
}
In OnBoardingView
Button {
path.append("UserTypeView")
}
label: {
Text("Hello")
}
.navigationDestination(for: String.self) { string in
UserTypeView(path: $path)
}
In UserTypeView
Button {
path.append("RegisterView")
}
label: {
Text("Hello")
}
.navigationDestination(for: String.self) { string in
RegisterView()
}
When the button on UserTypeView
is pressed, it navigates to UserTypeView
instead of RegisterView
.
Also, the Xcode logs saying
Only root-level navigation destinations are effective for a navigation stack with a homogeneous path.
Upvotes: 13
Views: 6465
Reputation: 307
Following @lorem ipsum example I think you can change this state variable @State private var path: NavigationPath = .init()
with an @ObservableObject
so you don't need to pass @Bindings
on all the views. You just pass it down from the CealUIApp
view as an WnvironmentObject
class NavigationStack: ObservableObject {
@Published var paths: NavigationPath = .init()
}
@main
struct CealUIApp: App {
let navstack = NavigationStack()
var body: some Scene {
WindowGroup {
AppEntry()
.environmentObject(navstack)
}
}
}
extension CealUIApp {
enum ScreenDestinations {
case userTypeView
case registerView
//Assign each case with a `View`
@ViewBuilder func view(_ path: Binding<NavigationPath>) -> some View {
switch self{
case .permissions:
UserTypeView()
case .seedPhrase:
RegisterView()
}
}
}
}
// An extra view after the AppView
struct AppEntry: View {
@EnvironmentObject var navStack: NavigationStack
var body: some View {
NavigationStack(path: $navStack.paths) {
OnBoardingView()
.navigationDestination(for: CealUIApp.ScreenDestinations.self) {
$0.view($navStack.paths)
}
}
}
}
And then the rest remain the same as @lorem ipsum said.
Upvotes: -1
Reputation: 2856
As an alternative to @lorem ipsum's answer, I'd suggest using NavigationLink
instead of a Button
as that will handle adding the values internal NavigationPath
for NavigationStack
. I would only add and pass around your own path if you wanted to do navigation programatically (for example after a network request).
First we have an enum to handle the possible routes and creation of their views:
enum Route {
case register
case userType
@ViewBuilder
var view: some View {
switch self {
case .register:
RegisterView()
case .userType:
UserTypeView()
}
}
}
Then we have the main app:
@main
struct CealUIApp: App {
var body: some Scene {
WindowGroup {
NavigationStack {
OnboardingView()
.navigationDestination(for: Route.self) { route in
route.view
}
}
}
}
}
And finally the implementation of those views with the various NavigationLink
in place:
struct OnboardingView: View {
var body: some View {
NavigationLink("Hello", value: Route.userType)
}
}
struct UserTypeView: View {
var body: some View {
NavigationLink("Hello", value: Route.register)
}
}
struct RegisterView: View {
var body: some View {
Text("Register View")
}
}
Upvotes: 9
Reputation: 29271
You can get rid of Only root-level navigation destinations are effective for a navigation stack with a homogeneous path
by changing the path type to NavigationPath
.
@State private var path: NavigationPath = .init()
But then you get a message/error that I think explains the issue better A navigationDestination for “Swift.String” was declared earlier on the stack. Only the destination declared closest to the root view of the stack will be used.
Apple has decided that scanning all views that are available is very inefficient so they will use the navigationDestination
will take priority.
Just imagine if your OnBoardingView
also had an option for "RegisterView"
.navigationDestination(for: String.self) { string in
switch string{
case "UserTypeView":
UserTypeView(path: $path)
case "RegisterView":
Text("fakie register view")
default:
Text("No view has been set for \(string)")
}
}
How would SwiftUI pick the right one?
So how to "fix"? You can try this alternative.
import SwiftUI
@available(iOS 16.0, *)
struct CealUIApp: View {
@State private var path: NavigationPath = .init()
var body: some View {
NavigationStack(path: $path){
OnBoardingView(path: $path)
.navigationDestination(for: ViewOptions.self) { option in
option.view($path)
}
}
}
//Create an `enum` so you can define your options
enum ViewOptions{
case userTypeView
case register
//Assign each case with a `View`
@ViewBuilder func view(_ path: Binding<NavigationPath>) -> some View{
switch self{
case .userTypeView:
UserTypeView(path: path)
case .register:
RegisterView()
}
}
}
}
@available(iOS 16.0, *)
struct OnBoardingView: View {
@Binding var path: NavigationPath
var body: some View {
Button {
//Append to the path the enum value
path.append(CealUIApp.ViewOptions.userTypeView)
} label: {
Text("Hello")
}
}
}
@available(iOS 16.0, *)
struct UserTypeView: View {
@Binding var path: NavigationPath
var body: some View {
Button {
//Append to the path the enum value
path.append(CealUIApp.ViewOptions.register)
} label: {
Text("Hello")
}
}
}
@available(iOS 16.0, *)
struct RegisterView: View {
var body: some View {
Text("Register")
}
}
@available(iOS 16.0, *)
struct CealUIApp_Previews: PreviewProvider {
static var previews: some View {
CealUIApp()
}
}
Upvotes: 15