Edward
Edward

Reputation: 2974

Change the root view of UIHostingController in SwiftUI

For a new SwiftUI iOS app, I do the following in the 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()
}

When a user hasn't signed up or logged in they are taken to the registration flow.

Once a user has signed up, how can I switch the RootView to go to my TabView? I can't seem to find any solution using SwiftUI.

Should I instead use an Environment object and listen for changes to the User's Auth Status?

Upvotes: 27

Views: 21551

Answers (6)

rony_y
rony_y

Reputation: 605

A little update to this answer you can also achieve the following by doing this :

You can make a Router class

First create an extension to 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)
    }

}

To get the key window.

Then Create a Router class and add these methods :

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

}

You can now call it from anywhere in your code for example from SceneDelegate :

Router.showMain(window: window)

Or without passing a window

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

And the transition will be animated.

Older Answer :

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

Usage :

Router.showMain()

And with this you can decide which window you want as your root at any given time.

Upvotes: 7

Parth Adroja
Parth Adroja

Reputation: 13514

In newer Xcode there are some SwiftUI template changes and below is how your initial view is loaded. Refer https://developer.apple.com/documentation/swiftui/app

@main
struct AuthCheckApp: App {
    var body: some Scene {
        WindowGroup {
            WelcomeView()
        }
    }
}

So in this case the first view is WelcomeView and this view is responsible to navigate to correct view it might be login, home

struct WelcomeView: View {
    
    @ObservedObject private var auth = Auth()
    
    var body: some View {
        if auth.token != nil {
            HomeView()
        } else {
            SignUpView(auth: auth)
        }
    }
}

Auth is a class confirming to ObservableObject protocol and has a published property called token. So when this token has a value it will load the HomeView in above case and in case of nil it will open the 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)
    }
}

This approach will be helpful in case if you have api dependency and need to wait for the token then you can use the WelcomeScreen as some splash or animation.

Upvotes: 1

Amrit Sidhu
Amrit Sidhu

Reputation: 1950

For some animation while changing the rootview use the below code in 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
    })

Upvotes: 1

Sai Amara
Sai Amara

Reputation: 91

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

By using this we can change the rootView in any button click by implementing the above code.

Upvotes: 9

zdravko zdravkin
zdravko zdravkin

Reputation: 2378

Very good answer LuLugaga, updated if you don't want to use @Observablebject so will not keep updating all the time, you can use Subject, as soon as you update token String, RootView will update.

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

Upvotes: 4

LuLuGaGa
LuLuGaGa

Reputation: 14388

Declare an AppRootView, something like this:

struct AppRootView: View {

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

and then in SceneDelegate set it as the root view:

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

You have to bind your view to your Auth() either by passing it in as I did above or by setting it on your environment. The beauty of SwiftUI is that as soon as the token is not nil, the view will redraw and your user will find them selves in MainTabbedView.

Upvotes: 40

Related Questions