只有根级别的导航目标对于具有同质路径的导航堆栈是有效的。

8
我正在尝试在我的SwiftUI应用程序中集成NavigationStack。
我有四个视图:CealUIAppOnBoardingViewUserTypeViewRegisterView
当用户在OnBoardingView中按下按钮时,我想从OnBoardingView导航到UserTypeView
并且,当用户在UserTypeView中按下按钮时,从UserTypeView 导航到RegisterView
以下是我的CealUIApp代码:
@main
struct CealUIApp: App {
    
    @State private var path = [String]()
    
    var body: some Scene {
        WindowGroup {
            NavigationStack(path: $path){
                OnBoardingView(path: $path)
            }
        }
    }
}

OnBoardingView
Button {
    path.append("UserTypeView")
}
label: {
    Text("Hello")
}
.navigationDestination(for: String.self) { string in
    UserTypeView(path: $path)
}

UserTypeView
Button {
    path.append("RegisterView")
}
label: {
    Text("Hello")
}
.navigationDestination(for: String.self) { string in
    RegisterView()
}

当在UserTypeView上的按钮被按下时,它会导航到UserTypeView而不是RegisterView
此外,Xcode日志显示仅适用于具有同质路径的导航堆栈的根级导航目标是有效的。
3个回答

10

通过将路径类型更改为 NavigationPath,您可以摆脱 只有根级别导航目标对具有同质路径的导航堆栈有效 的问题。

@State private var path: NavigationPath = .init()

但是,您收到了一条更好地解释了该问题的消息/错误:

导航目标“Swift.String”已在堆栈上早期声明。只有距离堆栈根视图最近声明的目标将被使用。

苹果公司决定扫描所有可用视图非常低效,因此将优先使用navigationDestination

想象一下,如果您的OnBoardingView还有一个"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)")
        }
        
    }

SwiftUI如何选择正确的选项?

那么如何“修复”?您可以尝试这个替代方法。

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()
    }
}

1
苹果的选择做出了很好的解释(+1)。除了在追加前更新ObservableObject之外,有没有其他方法将值发送到视图上?我认为这种方法会破坏视图自定义初始化的做法。 - kelalaka
@kelalaka 杀死了自定义的 init 吗?你可以使用 enum 传递几乎任何东西,Binding 是一种限制,但它是 navigationDestination 的限制,而不是这种方法的限制。 - lorem ipsum
你是指ViewOptions枚举吗?那会使编码变得复杂。是的,我在谈论navigationDestionation。在保持相同值的同时,我们不能调用不同的init。我尝试在另一个视图上使用不同的值,并遇到了这个警告。嗯,我认为observable是更好的解决方案,而且就我所看到的,它是单一来源-模型分离设计的核心。 - kelalaka
@kelalaka 为什么你不能使用不同的初始化?如果你遇到问题,可以提出问题,有人会帮助你。 - lorem ipsum

6
作为对 @lorem ipsum 答案的替代方案,我建议使用 NavigationLink 替代 Button,因为它会处理在 NavigationStack 中添加内部 NavigationPath 的值。只有当您想要进行编程导航(例如在网络请求后)时,才需要添加和传递自己的路径。
首先,我们有一个枚举来处理可能的路由和创建它们的视图:
enum Route {
    case register
    case userType
    
    @ViewBuilder
    var view: some View {
        switch self {
        case .register:
            RegisterView()
        case .userType:
            UserTypeView()
        }
    }
}

接下来是主要的应用程序:

@main
struct CealUIApp: App {
    var body: some Scene {
        WindowGroup {
            NavigationStack {
                OnboardingView()
                .navigationDestination(for: Route.self) { route in
                    route.view
                }
            }
        }
    }
}

最后,使用各种NavigationLink实现这些视图:

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")
    }
}

当点击NavigationLink时,我需要传递一个值。我该如何使用这种策略来实现? - undefined
您可以向Route枚举添加一个值,例如case userType(String),这样当您有导航链接时,它会是NavigationLink("Hello", value: Route.userType("your value here"))。然后在枚举中的视图构建器中,您可以获取该值。 - undefined

-1

参考 @lorem ipsum 的示例,我认为您可以使用 @ObservableObject 替换此状态变量 @State private var path: NavigationPath = .init(),这样您就不需要在所有视图中传递 @Bindings。您只需将其作为 WnvironmentObjectCealUIApp 视图传递下来即可。


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)
                }
        }
    }
}

然后其余部分与@lorem ipsum所说的一样。


你正在重新定义的 NavigationStack 已经存在于 SwiftUI 中。 - meaning-matters
不关心软件工程的基础并不明智。 - meaning-matters

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接