Integrate CYLTabBar center scan button and unify UIKit navigation.

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>
This commit is contained in:
2026-06-26 16:36:18 +08:00
parent 24a7339b68
commit a1c031c9b7
31 changed files with 838 additions and 517 deletions

View File

@ -7,19 +7,19 @@ App 模块负责应用入口、根视图切换、全局状态注入、主导航
该模块不直接处理具体页面业务,主要承担 App 级基础设施编排:
- 根据登录状态展示登录页、恢复态或主 Tab。
- 创建并注入全局状态和共享服务。
- 管理每个 Tab 独立的导航栈与 `AppRouter` 路径
- 管理每个 Tab 独立的 UIKit 导航栈,并通过 `AppNavigator` 统一执行全局跳转
- 协调登录成功、退出登录、冷启动恢复和本地缓存同步。
## 核心对象
- `AppDelegate` / `SceneDelegate`UIKit 应用入口挂载 `RootViewController`
- `AppDelegate` / `SceneDelegate`UIKit 应用入口`SceneDelegate` 挂载 `RootViewController` 并持有其强引用
- `AppServices`全局依赖容器Composition Root等价于参考工程 SwiftUI `RootView` 中的 Environment 注入;内部按 Session / Context / Network / UI / Runtime 分组,对外仍保留扁平属性访问。
- `RootViewController`根据 `AppSession.phase` 切换登录页、恢复占位或 `MainTabBarController`,并挂载全局 Toast / Loading。
- `RootViewController`监听 `AppSession.phase`,未登录时在自身上展示登录/恢复页,登录后将 `MainTabBarController` 设为 window 根控制器,并挂载全局 Toast / Loading。
- `AppSession`:保存认证阶段和正式 token只负责登录态不承载业务资料。
- `AccountContext`:保存当前账号资料、景区作用域和门店作用域。
- `PermissionContext`:保存角色权限、当前角色和扁平化权限 URI。
- `ScenicSpotContext`:保存当前景区下的景点/打卡点列表和加载状态。
- `AppRouter`:保存当前 Tab 和每个 Tab 自己的导航路径
- `AppNavigator`:应用级导航器,持有主 Tab 宿主弱引用,并以 UIKit 导航栈作为唯一导航事实源
- `ToastCenter`:管理当前全局 Toast 文案和自动隐藏任务。
- `AuthSessionCoordinator`:统一处理登录完成、退出登录、偏好读取和账号快照刷新。
- `SessionBootstrapper`:冷启动时读取本地 token 和账号快照,并向服务端校验登录态。
@ -44,7 +44,7 @@ App 模块负责应用入口、根视图切换、全局状态注入、主导航
普通业务 `ViewController`、模块基类(`ModuleTableViewController` / `ModuleCollectionViewController`)通过 `UIViewController.appServices` 扩展访问,**不必**再声明 `private let services` 或新增 `init(services:)`
导航辅助(`UIKitAppNavigation``HomeMenuRouting`)在全局 push 场景可直接使用 `AppServices.shared`
导航辅助(`HomeMenuRouting`)在全局 push 场景通过 `AppServices.shared.appNavigator` 执行跳转
### ViewModel 层边界
@ -69,7 +69,7 @@ ViewModel **不持有** `AppServices`。View 从容器取出具体依赖,在 `
5. 无 token 时保持 `loggedOut`,展示 `LoginViewController`
6. 有 token 时进入 `restoring`,先恢复本地账号快照。
7. `AccountContextLoader` 并行请求用户资料和角色权限,再补全景区与门店。
8. 校验成功后进入 `loggedIn`展示 `MainTabBarController`
8. 校验成功后进入 `loggedIn` `MainTabBarController` 设为 window 根控制器
9. `ScenicSpotContext` 按当前景区懒加载景点/打卡点。
10. 明确 token 失效时清空 token 和账号快照,回到登录页。
11. 普通网络失败时保留本地登录态,使用账号快照进入主界面。
@ -96,20 +96,20 @@ ViewModel **不持有** `AppServices`。View 从容器取出具体依赖,在 `
退出登录由 `AuthSessionCoordinator.logout` 统一处理:
- 清空 Keychain token。
- 清空账号快照。
- 重置账号上下文、权限上下文、景点上下文、导航路径和 Toast。
- 重置账号上下文、权限上下文、景点上下文、全部 UIKit 导航栈和 Toast。
- `AppSession` 切换为 `loggedOut`
- 保留上次手机号、协议状态等非敏感偏好。
## Toast
全局 Toast 由 `RootViewController` `viewDidAppear`挂载到 window业务页面通过 `showToast(...)``appServices.toastCenter.show(...)` 发出提示命令。
全局 Toast 由 `RootViewController`切换 window 根控制器后挂载到 window业务页面通过 `showToast(...)``appServices.toastCenter.show(...)` 发出提示命令。
Toast 展示为顶部全宽横幅,背景使用不透明主色并延伸到屏幕顶部、左边和右边;文案居中展示,不提供关闭按钮,默认 2.2 秒后自动消失。连续展示新 Toast 时会覆盖旧文案并重新计时,旧的自动隐藏任务不会影响新的 Toast。
## 导航规则
主界面使用 `MainTabBarController`,每个 Tab 内部由独立的 `TabNavigationController``UINavigationController`)包裹。`AppRouter` 为每个 `AppTab` 持有独立 `RouterPath`,切换 Tab 不会丢失该 Tab 的内部导航路径
主界面使用 `MainTabBarController` 作为 window 根控制器(登录后),每个 Tab 内部由独立的 `TabNavigationController``UINavigationController`)包裹。`UINavigationController.viewControllers` 是页面路径的唯一事实源,普通 Tab 切换保留该 Tab 的原有栈
Tab 根页面显示底部 TabBar。通过 `AppRoute` push 到子页面时,会根据 `AppRoute.hidesTabBarWhenPushed` 统一隐藏 TabBar。
跨 Tab 跳转通过 `AppRouter.select` / `navigateHome` / `navigateProfile` 处理Tab 内 push 通过 `UIKitAppNavigation` 或当前 Nav 栈完成。新增页面时优先扩展 `AppRoute` / `HomeRoute``AppRouteViewControllerFactory` 注册对应 ViewController。
跨 Tab、推送通知、UI Test 直达和全局扫码都通过 `AppNavigator` 处理。`AppNavigationPolicy.currentStack` 沿当前栈 push`.tabRoot` 切到目标 Tab 并先回到 root`.preserveTab` 只切换 Tab 并保留原栈。新增页面时优先扩展 `AppRoute` / `HomeRoute` / `ProfileRoute` / `OrdersRoute`,并`AppRouteViewControllerFactory` 注册对应 ViewController。