使用SwiftUI编程方式在程序中导航到新视图

30

描述性示例:

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

如何在SwiftUI中实现这样的自动转换?

7个回答

32

成功登录后,您可以使用自己的登录视图替换下一个视图。例如:

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

1
@zgorawski 没错。我还没有找到用SwiftUI手动推送新视图的方法。这种方式还可以确保用户无法返回登录屏幕,除非您将isLoggedin设置为false。 - M Reza
6
@MoRezaFarahani,我正在使用Beta 7版本,UserAuth现在是一个ObservableObject。在我的ContentView中,我在"var body: some View"这一行中遇到了错误:"函数声明了不透明的返回类型,但是在其主体中没有返回语句用于推断底层类型"。 - 39fredy
2
我对SwiftUI还很陌生,但我按照这个步骤操作后出现了错误:致命错误:找不到类型为UserAuth的ObservableObject。可能缺少一个View.environmentObject(_:)来作为此视图的祖先。该错误仅在运行时显示。 - Cory
1
我也遇到了"No ObservableObject of type found"的错误。在上面的代码中,AuthUser是在哪里实例化的?这个类的某个版本是否可以在任何视图中访问,而不需要传递它? - alionthego
1
其他人面临的相同问题:找不到类型为ObservableObject的错误,有什么解决方法,有人可以详细说明一下吗? - tintin
显示剩余5条评论

7

为了以后的参考,许多用户报告出现了“函数声明不透明返回类型”的错误,要实现上述代码(@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。


2
这个动画能正确地显示吗?我希望在这两个视图之间有一个推送动画。 - Zorayr

6
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 绑定变量的导航链接


1
感谢@David Rozmajzl,之后我只是将用户是否已登录存储在UserDefaults中,然后从sceneDelegate中根据该布尔值选择哪个视图。 - FamousMaxy
"init(destination:isActive:label:)"在iOS 16.0中已被弃用。 - Pete Streem

4

基于 Swift 5.2 的更新,结合变化说明如下:可以使用发布者(publishers)简化处理。

  1. 创建一个名为 UserAuth 的类,如下所示,不要忘记导入 import Combine
class UserAuth: ObservableObject {
        @Published var isLoggedin:Bool = false

        func login() {
            self.isLoggedin = true
        }
    }
  1. Update SceneDelegate.Swift with

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

  2. Your authentication view

     struct LoginView: View {
        @EnvironmentObject  var  userAuth: UserAuth
        var body: some View {
            ...
        if ... {
        self.userAuth.login()
        } else {
        ...
        }
     }
    }
    
    
  3. Your dashboard after successful authentication, if the authentication userAuth.isLoggedin = true then it will be loaded.

       struct NextView: View {
         var body: some View {
         ...
         }
       }
    
  4. Lastly, the initial view to be loaded once the application is launched.

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

这个可以工作。我按照被接受的答案去做,但一开始它并没有起作用。@Published 的目的是什么? - aldoblack
@aldoblack 这是正确的方式!@Published 旨在与 SwiftUI 一起使用。 - LetsGoBrandon

2

这里有一个关于UINavigationController的扩展,它具有简单的推入/弹出功能,并且能够得到正确的 SwiftUI 视图动画。我对大多数自定义导航的问题在于推入/弹出动画不对。使用带有isActive绑定的NavigationLink是正确的方法,但它不够灵活或可扩展。因此,下面的扩展对我很有用:

/**
 * 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()))

1
我跟随Gene的答案,但它存在两个问题,我在下面进行了修复。第一个问题是变量isLoggedIn必须具有属性@Published才能按预期工作。第二个问题是如何实际使用环境对象。
对于第一个问题,请将UserAuth.isLoggedIn更新如下:
@Published var isLoggedin = false {
didSet {
  didChange.send(self)
}

第二个问题是如何实际使用环境对象。这并不是Gene的回答有问题,只是我在评论中注意到了很多关于它的问题,而我没有足够的声望来回答它们。将此添加到您的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)

-1
现在,您只需要简单地创建一个新视图的实例,并将其放入 NavigationButton 中即可导航到该视图:
NavigationButton(destination: NextView(), isDetail: true, onTrigger: { () -> Bool in
    return self.done
}) {
    Text("Login")
}

如果您在 onTrigger 中返回 true,则表示您已成功登录用户。

SwiftUI 2 中不存在 NavigationButton。最好删除该答案。 - Andrew_STOP_RU_WAR_IN_UA

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