// // RootViewController.swift // suixinkan // import SnapKit import UIKit @MainActor /// 应用根协调器,监听登录阶段并在 window 根层级切换登录页与 `MainTabBarController`。 final class RootViewController: UIViewController { private let services: AppServices private var currentAuthChild: UIViewController? /// 初始化实例。 init(services: AppServices = .shared) { self.services = services super.init(nibName: nil, bundle: nil) } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } /// 初始化视图、监听登录阶段变化并启动会话恢复。 override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .systemBackground services.appSession.onChange = { [weak self] in self?.handleSessionPhaseChange() } applyRoot(for: services.appSession.phase) bootstrapSession() services.configurePushNotifications() } /// 冷启动时恢复本地会话,并在已登录时申请推送权限。 private func bootstrapSession() { Task { await services.globalLoading.withLoading { await services.sessionBootstrapper.restore( appSession: services.appSession, accountContext: services.accountContext, permissionContext: services.permissionContext, profileAPI: services.profileAPI, accountContextAPI: services.accountContextAPI, toastCenter: services.toastCenter ) } if services.appSession.isLoggedIn, !AppUITestLaunchState.isRunningUITests { PushNotificationManager.shared.requestAuthorizationAndRegister() } } } /// 登录阶段变化时切换 window 根控制器,并在登出时清理各上下文状态。 private func handleSessionPhaseChange() { applyRoot(for: services.appSession.phase) switch services.appSession.phase { case .loggedIn: if !AppUITestLaunchState.isRunningUITests { PushNotificationManager.shared.requestAuthorizationAndRegister() } Task { await reloadScenicSpotContextIfNeeded() } case .loggedOut: // 登出后重置账号、权限、路由与排队运行时,避免残留旧状态 services.accountContext.reset() services.permissionContext.reset() services.scenicSpotContext.reset() services.appNavigator.resetAllStacks() services.appNavigator.detach() services.toastCenter.dismiss() services.scenicQueueRuntime.stop() case .restoring: break } } /// 登录成功后按当前景区 ID 刷新景区上下文。 private func reloadScenicSpotContextIfNeeded() async { guard services.appSession.isLoggedIn else { services.scenicSpotContext.reset() return } await services.scenicSpotContext.reload( scenicId: services.accountContext.currentScenic?.id, api: services.accountContextAPI ) } /// 按登录阶段切换 window 根控制器。 private func applyRoot(for phase: AuthPhase) { switch phase { case .loggedIn: showMainTabBarAsWindowRoot() case .loggedOut: showAuthRoot(with: LoginViewController(services: services)) case .restoring: showAuthRoot(with: restoringViewController()) } } /// 登录后将 `MainTabBarController` 设为 window 根控制器,满足 CYLTabBarController 挂载要求。 private func showMainTabBarAsWindowRoot() { removeCurrentAuthChild() guard let window = activeWindow else { return } let mainTabBarController = MainTabBarController(services: services) window.rootViewController = mainTabBarController attachGlobalOverlays(to: window) } /// 未登录或恢复阶段展示登录/占位页,由当前协调器承载。 private func showAuthRoot(with child: UIViewController) { guard let window = activeWindow else { mountAuthChild(child) return } if window.rootViewController !== self { window.rootViewController = self } mountAuthChild(child) attachGlobalOverlays(to: window) } /// 将登录页或恢复占位页挂载为当前协调器的子控制器。 private func mountAuthChild(_ child: UIViewController) { guard currentAuthChild !== child else { return } currentAuthChild?.willMove(toParent: nil) currentAuthChild?.view.removeFromSuperview() currentAuthChild?.removeFromParent() addChild(child) view.addSubview(child.view) child.view.snp.makeConstraints { make in make.edges.equalToSuperview() } child.didMove(toParent: self) currentAuthChild = child } /// 移除当前认证阶段子控制器。 private func removeCurrentAuthChild() { currentAuthChild?.willMove(toParent: nil) currentAuthChild?.view.removeFromSuperview() currentAuthChild?.removeFromParent() currentAuthChild = nil } /// 会话恢复阶段的空白占位页,避免闪烁登录页。 private func restoringViewController() -> UIViewController { let controller = UIViewController() controller.view.backgroundColor = .systemBackground return controller } /// 将全局 Toast 与 Loading 视图附加到指定 window。 private func attachGlobalOverlays(to window: UIWindow) { services.toastCenter.attachToWindow(window) services.globalLoading.attachToWindow(window) } /// 获取当前 key window。 private var activeWindow: UIWindow? { view.window ?? UIApplication.shared.connectedScenes .compactMap { $0 as? UIWindowScene } .flatMap(\.windows) .first(where: \.isKeyWindow) } }