The answer by @kontiki is probably the most SwiftUI-y, but I will present a different solution, probably not as good! But maybe more flexible/scalable.
You can swap rootView of UIHostingController:
SceneDelegate
class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? fileprivate lazy var appCoordinator: AppCoordinator = { let rootViewController: UIHostingController<AnyView> = .init(rootView: EmptyView().eraseToAny()) window?.rootViewController = rootViewController let navigationHandler: (AnyScreen, TransitionAnimation) -> Void = { [unowned rootViewController, window] (newRootScreen: AnyScreen, transitionAnimation: TransitionAnimation) in UIView.transition( with: window!, duration: 0.5, options: transitionAnimation.asUIKitTransitionAnimation, animations: { rootViewController.rootView = newRootScreen }, completion: nil ) } return AppCoordinator( dependencies: ( securePersistence: KeyValueStore(KeychainSwift()), preferences: .default ), navigator: navigationHandler ) }() func scene( _ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions ) { self.window = .fromScene(scene) appCoordinator.start() } } enum TransitionAnimation { case flipFromLeft case flipFromRight } private extension TransitionAnimation { var asUIKitTransitionAnimation: UIView.AnimationOptions { switch self { case .flipFromLeft: return UIView.AnimationOptions.transitionFlipFromLeft case .flipFromRight: return UIView.AnimationOptions.transitionFlipFromRight } } }
AppCoordinator
And here is the AppCoordinator:
final class AppCoordinator { private let preferences: Preferences private let securePersistence: SecurePersistence private let navigationHandler: (AnyScreen, TransitionAnimation) -> Void init( dependencies: (securePersistence: SecurePersistence, preferences: Preferences), navigator navigationHandler: @escaping (AnyScreen, TransitionAnimation) -> Void ) { self.preferences = dependencies.preferences self.securePersistence = dependencies.securePersistence self.navigationHandler = navigationHandler } } // MARK: Internal internal extension AppCoordinator { func start() { navigate(to: initialDestination) } } // MARK: Destination private extension AppCoordinator { enum Destination { case welcome, getStarted, main } func navigate(to destination: Destination, transitionAnimation: TransitionAnimation = .flipFromLeft) { let screen = screenForDestination(destination) navigationHandler(screen, transitionAnimation) } func screenForDestination(_ destination: Destination) -> AnyScreen { switch destination { case .welcome: return AnyScreen(welcome) case .getStarted: return AnyScreen(getStarted) case .main: return AnyScreen(main) } } var initialDestination: Destination { guard preferences.hasAgreedToTermsAndPolicy else { return .welcome } guard securePersistence.isAccountSetup else { return .getStarted } return .main } } // MARK: - Screens private extension AppCoordinator { var welcome: some Screen { WelcomeScreen() .environmentObject( WelcomeViewModel( preferences: preferences, termsHaveBeenAccepted: { [unowned self] in self.start() } ) ) } var getStarted: some Screen { GetStartedScreen() .environmentObject( GetStartedViewModel( preferences: preferences, securePersistence: securePersistence, walletCreated: { [unowned self] in self.navigate(to: .main) } ) ) } var main: some Screen { return MainScreen().environmentObject( MainViewModel( preferences: preferences, securePersistence: securePersistence, walletDeleted: { [unowned self] in self.navigate(to: .getStarted, transitionAnimation: .flipFromRight) } ) ) } }