在SwiftUI中更改UIHostingController的根视图

27

对于一个新的 SwiftUI iOS 应用程序,在 SceneDelegate 中我会按照以下步骤进行操作

if let windowScene = scene as? UIWindowScene {
    let window = UIWindow(windowScene: windowScene)
    if Auth().token == nil {
        window.rootViewController = UIHostingController(rootView: StartRegistrationView())
    } else {
        window.rootViewController = UIHostingController(rootView: MainTabbedView())
    }
    self.window = window
    window.makeKeyAndVisible()
}

当用户没有注册或登录时,他们将进入注册流程。

一旦用户注册成功后,我如何切换根视图以进入我的选项卡视图?我似乎找不到使用SwiftUI的解决方案。

我是否应该使用Environment对象并监听用户的Auth状态的更改?

6个回答

40

声明一个AppRootView,类似这样:

struct AppRootView: View {

    @ObservedObject private var auth: Auth
    var body: some View {
        Group {
            if auth.token != nil {
                MainTabbedView()
            } else {
                StartRegistrationView()
            }
        }
    }
}

然后在SceneDelegate中将其设置为根视图:

window.rootViewController = UIHostingController(rootView: AppRootView(auth: $auth))

你必须将你的视图与Auth()绑定,可以像我上面所做的那样通过传递它来实现,也可以在环境中设置它。 SwiftUI的美妙之处在于一旦令牌不为空,视图就会重新绘制,你的用户将会发现自己在MainTabbedView中。


2
有什么想法吗:属性类型“Auth”与其包装类型“ObservedObject”的“wrappedValue”属性不匹配。 - DanielSon
你的认证类型是否实现了ObservableObject? - bkocik
是的,它必须是一个ObservableObject。 - LuLuGaGa
很棒的方法,但有没有办法使这种视图变化动起来?我自己做不到。 - Lachezar Todorov
@LuLuGaGa:你能分享一个 GitHub 链接的示例吗? - Monish
显示剩余2条评论

9
let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene

if let windowScenedelegate = scene?.delegate as? SceneDelegate {
   let window = UIWindow(windowScene: scene!)
   window.rootViewController = UIHostingController(rootView:ContentView())
   windowScenedelegate.window = window
   window.makeKeyAndVisible()
}

通过实现上面的代码,我们可以在任何按钮点击时更改rootView。


这个部分有效。在新视图上的按钮直到应用程序重新启动后才会响应。有没有办法解决这个问题? - lazerrouge

7

对于这个答案的一个小更新,你还可以通过以下方式实现:

你可以创建一个路由器(Router)

首先,创建一个扩展UIApplication

import Foundation
import UIKit

extension UIApplication {
    
    var keyWindow: UIWindow? {
        return UIApplication.shared.connectedScenes
            .filter { $0.activationState == .foregroundActive }
            .first(where: { $0 is UIWindowScene })
            .flatMap({ $0 as? UIWindowScene })?.windows
            .first(where: \.isKeyWindow)
    }

}

获取主窗口的方法

然后创建一个路由器(Router)类并添加以下方法:

import Foundation
import SwiftUI

final class Router {

    //MARK: Main flow.
    public static func showMain(window: UIWindow? = nil) {
        Router.setRootView(view: MainView(), window: window)
    }

    //MARK: private
    private static func setRootView<T: View>(view: T, window: UIWindow? = nil) {
        if window != nil {
            window?.rootViewController = UIHostingController(rootView: view)
            UIView.transition(with: window!,
                              duration: 0.3,
                              options: .transitionCrossDissolve,
                              animations: nil,
                              completion: nil)
            return
        }else {
            UIApplication.shared.keyWindow?.rootViewController = UIHostingController(rootView: view)
            UIView.transition(with: UIApplication.shared.keyWindow!,
                              duration: 0.3,
                              options: .transitionCrossDissolve,
                              animations: nil,
                              completion: nil)
        }
    }

}

你现在可以从你代码中的任何位置调用它,例如从SceneDelegate中:
Router.showMain(window: window)

或者不使用 window 参数

Router.showMain() //if the window is nil it will go to the else statement and use UIApplication.keyWindow.

并且过渡将会有动画效果。

旧答案:

import Foundation
import UIKit
import SwiftUI

class Router {
    
    class var window: UIWindow? {
        if let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
            if let sceneDelegate = scene.delegate as? SceneDelegate {
                let window = UIWindow(windowScene: scene)
                sceneDelegate.window = window
                window.makeKeyAndVisible()
                return window
            }
        }
        return nil
    }
    
    static func showMain() {
        window?.rootViewController = UIHostingController(rootView: ContentView())
    }
    
}

使用方法:

Router.showMain()

使用这个功能,你可以在任何时候决定哪个窗口是你的根窗口。


4

非常好的回答LuLugaga,如果您不想使用@Observablebject,以免一直更新,您可以使用Subject,只要您更新令牌字符串,RootView 将会更新。

struct RootView: View {

    var loginViewModel: LoginViewModel = LoginViewModel()

    @State var tokenString = ""

    var body: some View {
        Group {
            tokenString.count > 0 ? AnyView(ContentView(model: playerViewModel)) :  AnyView(LoginView(loginViewModel: loginViewModel))
        }.onReceive(loginViewModel.tokenString) {
            self.tokenString = $0
        }
    }
}


class LoginViewModel {

    let tokenString = PassthroughSubject<String, Never>()

    var token: String {
        get { return "" }
    set {
        self.tokenString.send(newValue)
    }
}

1
在较新的Xcode中,有一些SwiftUI模板更改,以下是如何加载您的初始视图。请参考https://developer.apple.com/documentation/swiftui/app
@main
struct AuthCheckApp: App {
    var body: some Scene {
        WindowGroup {
            WelcomeView()
        }
    }
}

所以在这种情况下,第一个视图是WelcomeView,该视图负责导航到正确的视图,可能是登录、主页。
struct WelcomeView: View {
    
    @ObservedObject private var auth = Auth()
    
    var body: some View {
        if auth.token != nil {
            HomeView()
        } else {
            SignUpView(auth: auth)
        }
    }
}

Auth是一个符合ObservableObject协议的类,具有名为token的已发布属性。因此,当该token具有值时,它将加载上述情况下的HomeView,在nil的情况下,它将打开SignUpView。

class Auth: ObservableObject {
    @Published var token: String?
}


struct SignUpView: View {
    
    let auth: Auth
    
    var body: some View {
        VStack {
            Text("Hello, please click below button to login")
                .padding()
            Button("Login") {
                print("Loogin Tapped")
                auth.token = "TOKEN"
            }
        }
    }
}

struct HomeView: View {
    var body: some View {
        Text("Welcome Parth!")
            .padding()
            .background(Color.red)
    }
}

这种方法在你有API依赖并需要等待令牌时会很有帮助,你可以使用WelcomeScreen作为启动画面或动画。

1

如果想在更改根视图时添加一些动画,请在sceneDelegate中使用以下代码:

    window.rootViewController = UIHostingController(rootView: HomeView())

    // A mask of options indicating how you want to perform the animations.
    let options: UIView.AnimationOptions = .transitionCrossDissolve

    // The duration of the transition animation, measured in seconds.
    let duration: TimeInterval = 0.3

    // Creates a transition animation.
    UIView.transition(with: window, duration: duration, options: options, animations: {}, completion:
    { completed in
        // maybe do something on completion here
    })

当在按钮点击时更改根视图控制器时,新视图上的按钮变得无响应。有没有办法解决这个问题? - lazerrouge
1
不响应与这种方法无关。我相信可能是由于按钮的zindex。尝试更改按钮的索引。".zindex(1)" - Amrit Sidhu

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