以编程方式导航到 SwiftUI 中的新视图

Posted

技术标签:

【中文标题】以编程方式导航到 SwiftUI 中的新视图【英文标题】:Programmatically navigate to new view in SwiftUI 【发布时间】:2019-06-25 12:30:39 【问题描述】:

描述性示例:

登录屏幕,用户点击“登录”按钮,请求被执行,UI 显示等待指示器,然后在成功响应后我想自动将用户导航到下一个屏幕。

如何在 SwiftUI 中实现这种自动过渡?

【问题讨论】:

【参考方案1】:

您可以在成功登录后将下一个视图替换为您的登录视图。例如:

struct LoginView: View 
    var body: some View 
        ...
    


struct NextView: View 
    var body: some View 
        ...
    


// Your starting view
struct ContentView: View 

    @EnvironmentObject var userAuth: UserAuth 

    var body: some View 
        if !userAuth.isLoggedin 
            LoginView()
         else 
            NextView()
        

    

您应该在数据模型中处理您的登录过程,并使用诸如@EnvironmentObject 之类的绑定将isLoggedin 传递给您的视图。

注意:在 Xcode Version 11.0 beta 4 中,为了符合协议'BindableObject',必须添加willChange 属性

import Combine

class UserAuth: ObservableObject 

  let didChange = PassthroughSubject<UserAuth,Never>()

  // required to conform to protocol 'ObservableObject' 
  let willChange = PassthroughSubject<UserAuth,Never>()

  func login() 
    // login request... on success:
    self.isLoggedin = true
  

  var isLoggedin = false 
    didSet 
      didChange.send(self)
    

    // willSet 
    //       willChange.send(self)
    // 
  

【讨论】:

如果我正确理解了您的概念,这就像在视图树的根目录上安装了两个单独的层次结构,一个用于经过身份验证的案例,一个用于未经身份验证的情况 @zgorawski 没错。我还没有找到使用 SwiftUI 手动推送新视图的方法。这种方式还可以确保用户无法导航回登录屏幕,除非您将 isLoggedin 设置为 false。 这也是我在 Flutter 中所做的解决方案。不幸的是,它仅在登录示例或类似的“全局状态”案例中才是优雅的。但是,如果你想例如。在详细信息视图中确认某些数据版本,显示微调器,然后显示错误或关闭详细信息视图 - 这不会那么简单。 @MoRezaFarahani 我正在使用 beta 7,而 UserAuth 现在是一个 ObservableObject。在我的 ContentView 中,我在“var body: some View”行中收到错误“函数声明了一个不透明的返回类型,但在其主体中没有返回语句来推断基础类型” 我是 SwiftUI 的新手,但我完全遵循了这个并得到了这个错误:致命错误:没有找到 UserAuth 类型的 ObservableObject。 UserAuth 的 View.environmentObject(_:) 作为此视图的祖先可能会丢失。:文件 SwiftUI,第 0 行。此错误仅在运行时显示。【参考方案2】:

为了将来参考,由于许多用户报告收到错误“函数声明一个不透明的返回类型”,从@MoRezaFarahani 实现上述代码需要以下语法:

struct ContentView: View 

    @EnvironmentObject var userAuth: UserAuth 

    var body: some View 
        if !userAuth.isLoggedin 
            return AnyView(LoginView())
         else 
            return AnyView(NextView())
        

    

这适用于 Xcode 11.4 和 Swift 5

【讨论】:

这个动画正确吗?我想要两个视图之间的推送动画。【参考方案3】:
struct LoginView: View 
    
    @State var isActive = false
    @State var attemptingLogin = false
    
    var body: some View 
        ZStack 
            NavigationLink(destination: HomePage(), isActive: $isActive) 
                Button(action: 
                    attlempinglogin = true
                    // Your login function will most likely have a closure in 
                    // which you change the state of isActive to true in order 
                    // to trigger a transition
                    loginFunction()  response in
                        if response == .success 
                            self.isActive = true
                         else 
                            self.attemptingLogin = false
                        
                    
                ) 
                    Text("login")
                
            
            
            WaitingIndicator()
                .opacity(attemptingLogin ? 1.0 : 0.0)
        
    

使用带有 $isActive 绑定变量的导航链接

【讨论】:

【参考方案4】:

根据 Swift Version 5.2 的 combine 更改来阐述其他人在上面阐述的内容,可以使用发布者进行简化。

    创建一个类名UserAuth如下图别忘了导入import Combine
class UserAuth: ObservableObject 
        @Published var isLoggedin:Bool = false

        func login() 
            self.isLoggedin = true
        
    

    更新SceneDelegate.Swift

    let contentView = ContentView().environmentObject(UserAuth())

    您的身份验证视图

     struct LoginView: View 
        @EnvironmentObject  var  userAuth: UserAuth
        var body: some View 
            ...
        if ... 
        self.userAuth.login()
         else 
        ...
        
     
    
    
    

    认证成功后你的dashboard,如果认证userAuth.isLoggedin = true那么就会被加载。

       struct NextView: View 
         var body: some View 
         ...
         
       
    

    最后,应用程序启动后要加载的初始视图。

struct ContentView: View 
    @EnvironmentObject var userAuth: UserAuth 
    var body: some View 
        if !userAuth.isLoggedin 
                LoginView()
             else 
                NextView()
            
    
  

【讨论】:

这个有效。我遵循了接受的答案,但它没有首先起作用。 @Published 的目的是什么? @aldoblack 这是正确的方法! @Published 旨在与 SwiftUI 一起使用。【参考方案5】:

这是UINavigationController 上的一个扩展,它具有简单的推送/弹出功能,带有 SwiftUI 视图,可以获得正确的动画。上面大多数自定义导航的问题是推送/弹出动画已关闭。将NavigationLinkisActive 绑定一起使用是正确的做法,但它不灵活或不可扩展。所以下面的扩展对我有用:

/**
 * Since SwiftUI doesn't have a scalable programmatic navigation, this could be used as
 * replacement. It just adds push/pop methods that host SwiftUI views in UIHostingController.
 */
extension UINavigationController: UINavigationControllerDelegate 

    convenience init(rootView: AnyView) 
        let hostingView = UIHostingController(rootView: rootView)
        self.init(rootViewController: hostingView)

        // Doing this to hide the nav bar since I am expecting SwiftUI
        // views to be wrapped in NavigationViews in case they need nav.
        self.delegate = self
    

    public func pushView(view:AnyView) 
        let hostingView = UIHostingController(rootView: view)
        self.pushViewController(hostingView, animated: true)
    

    public func popView() 
        self.popViewController(animated: true)
    

    public func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) 
        navigationController.navigationBar.isHidden = true
    

下面是一个使用 window.rootViewController 的简单示例。

var appNavigationController = UINavigationController.init(rootView: rootView)
window.rootViewController = appNavigationController
window.makeKeyAndVisible()

// Now you can use appNavigationController like any UINavigationController, but with SwiftUI views i.e. 
appNavigationController.pushView(view: AnyView(MySwiftUILoginView()))

【讨论】:

【参考方案6】:

我遵循了 Gene 的回答,但我在下面修复了两个问题。首先是变量 isLoggedIn 必须具有属性 @Published 才能按预期工作。二是如何实际使用环境对象。

首先,将 UserAuth.isLoggedIn 更新为以下内容:

@Published var isLoggedin = false 
didSet 
  didChange.send(self)

第二个是如何实际使用环境对象。这在 Gene 的回答中并没有错,我只是在 cmets 中注意到很多关于它的问题,而我没有足够的业力来回应他们。将此添加到您的 SceneDelegate 视图中:

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) 
    // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
    // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
    // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
    var userAuth = UserAuth()
    
    // Create the SwiftUI view that provides the window contents.
    let contentView = ContentView().environmentObject(userAuth)

【讨论】:

【参考方案7】:

现在您只需要简单地创建一个您想要导航到的新视图的实例并将其放入 NavigationButton:

NavigationButton(destination: NextView(), isDetail: true, onTrigger:  () -> Bool in
    return self.done
) 
    Text("Login")

如果你返回true onTrigger 表示你成功登录用户。

【讨论】:

导航按钮在 SwiftUI 2 中不存在。最好删除答案。

以上是关于以编程方式导航到 SwiftUI 中的新视图的主要内容,如果未能解决你的问题,请参考以下文章

以编程方式一个接一个地关闭 SwiftUI 视图不起作用

Push、Segue、Summon、导航以编程方式查看 SwiftUI

SwiftUI 以编程方式从可表示返回到视图

navigationBarItems 中的 SwiftUI NavigationButton

SwiftUI NavigationLink 弹出

在 Swift 中单击以编程方式切换到 TableViewCell 上的导航视图