Add CYLTabBarController with a global scan entry, promote MainTabBarController to the window root after login, replace RouterPath-based navigation with AppNavigator, and fix Profile diffable cell reconfiguration crashes. Co-authored-by: Cursor <cursoragent@cursor.com>
181 lines
6.1 KiB
Swift
181 lines
6.1 KiB
Swift
//
|
||
// 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)
|
||
}
|
||
}
|