Files
suixinkan_ios_uikit/suixinkan_ios/App/App.md
汉秋 1a6d4a1791 Fix home navigation bar visibility and hide scan tab title.
Home uses a custom top bar, so TabNavigationController now syncs system nav bar visibility with the stack top; remove the scan tab text label to show icon only.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 08:59:31 +08:00

118 lines
7.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# App 模块业务逻辑
## 模块职责
App 模块负责应用入口、根视图切换、全局状态注入、主导航状态、登录态恢复和全局 Toast / Loading。
该模块不直接处理具体页面业务,主要承担 App 级基础设施编排:
- 根据登录状态展示登录页、恢复态或主 Tab。
- 创建并注入全局状态和共享服务。
- 管理每个 Tab 独立的 UIKit 导航栈,并通过 `AppNavigator` 统一执行全局跳转。
- 协调登录成功、退出登录、冷启动恢复和本地缓存同步。
## 核心对象
- `AppDelegate` / `SceneDelegate`UIKit 应用入口;`SceneDelegate` 挂载 `RootViewController` 并持有其强引用。
- `AppServices`全局依赖容器Composition Root等价于参考工程 SwiftUI `RootView` 中的 Environment 注入;内部按 Session / Context / Network / UI / Runtime 分组,对外仍保留扁平属性访问。
- `RootViewController`:监听 `AppSession.phase`,未登录时在自身上展示登录/恢复页,登录后将 `MainTabBarController` 设为 window 根控制器,并挂载全局 Toast / Loading。
- `AppSession`:保存认证阶段和正式 token只负责登录态不承载业务资料。
- `AccountContext`:保存当前账号资料、景区作用域和门店作用域。
- `PermissionContext`:保存角色权限、当前角色和扁平化权限 URI。
- `ScenicSpotContext`:保存当前景区下的景点/打卡点列表和加载状态。
- `AppNavigator`:应用级导航器,持有主 Tab 宿主弱引用,并以 UIKit 导航栈作为唯一导航事实源。
- `ToastCenter`:管理当前全局 Toast 文案和自动隐藏任务。
- `AuthSessionCoordinator`:统一处理登录完成、退出登录、偏好读取和账号快照刷新。
- `SessionBootstrapper`:冷启动时读取本地 token 和账号快照,并向服务端校验登录态。
- `AccountContextLoader`:统一同步用户资料、角色权限、景区和门店。
## AppServices 依赖约定
`AppServices.shared` 是唯一 Composition Root集中创建 `APIClient`、各业务 API 与全局 Context并绑定 token provider。
### 何时通过 `init(services:)` 注入
仅在 Composition 边界传参,便于 UI Test 或单元测试替换容器:
- `RootViewController`(默认 `AppServices.shared`
- `MainTabBarController``TabNavigationController`
- `LoginViewController`
- `AppRouteViewControllerFactory.makeViewController(..., services:)`
根链路使用 `init(services: AppServices = .shared)`,生产代码无感,测试可传入自定义实例。
### 何时直接使用 `appServices`
普通业务 `ViewController`、模块基类(`ModuleTableViewController` / `ModuleCollectionViewController`)通过 `UIViewController.appServices` 扩展访问,**不必**再声明 `private let services` 或新增 `init(services:)`
导航辅助(如 `HomeMenuRouting`)在全局 push 场景通过 `AppServices.shared.appNavigator` 执行跳转。
### ViewModel 层边界
ViewModel **不持有** `AppServices`。View 从容器取出具体依赖,在 `reload` / `load` / `submit` 等方法调用时传入所需 API 与 Context。
### 适合独立单例的类型
以下类型独立于 `AppServices`,因其生命周期或系统入口特殊:
- `PushNotificationManager.shared``AppDelegate` 回调桥
- `CloudTransferStore.shared`:跨页面云传任务状态
- 第三方 SDK`AMapServices`
`AppSession``AccountContext`、各 `*API` 等**不应**拆成多个 `.shared`,须由 `AppServices` 统一创建,并在登出时由 `RootViewController` 统一 reset。
## 启动流程
1. `AppDelegate` 创建 `RootViewController(services: .shared)`
2. `AppServices` 初始化共享依赖(`NetworkBundle` 内共享同一 `APIClient`)。
3. `APIClient` 绑定 `AppSession.token` 作为默认 token provider。
4. `SessionBootstrapper.restore` 尝试从 Keychain 读取正式 token。
5. 无 token 时保持 `loggedOut`,展示 `LoginViewController`
6. 有 token 时进入 `restoring`,先恢复本地账号快照。
7. `AccountContextLoader` 并行请求用户资料和角色权限,再补全景区与门店。
8. 校验成功后进入 `loggedIn`,将 `MainTabBarController` 设为 window 根控制器。
9. `ScenicSpotContext` 按当前景区懒加载景点/打卡点。
10. 明确 token 失效时清空 token 和账号快照,回到登录页。
11. 普通网络失败时保留本地登录态,使用账号快照进入主界面。
## UI Test 启动约定
- `AppUITestLaunchState` 在收到 `-suixinkan-ui-tests` 时跳过推送注册和排队 WebSocket避免系统弹窗干扰自动化。
- `-suixinkan-ui-tests-reset-state` 用于冷启动清理 Keychain 与 UserDefaults。
- `-suixinkan-ui-tests-open-menu <菜单标题>` 登录后直达首页调试目录中的目标页。
- `-suixinkan-ui-tests-open-profile <路由名>` 登录后直达个人中心二级页(如 `settings``realNameAuth`)。
- `AppUITestRouteDriver` 仅在 DEBUG 构建下解析上述直达参数,供 XCUITest 逐页验证。
- 详细运行方式见 `suixinkanUITests/README.md`
## 登录和退出
登录完成由 `AuthSessionCoordinator.completeLogin` 统一处理:
- 正式 token 写入 `SessionTokenStore`
- 上次手机号和协议状态写入 `AppPreferencesStore`
- 账号资料、景区列表、门店列表写入 `AccountContext`
- 角色权限和当前角色写入 `PermissionContext`
- 非敏感账号快照写入 `AccountSnapshotStore`
- `AppSession` 切换为 `loggedIn`
退出登录由 `AuthSessionCoordinator.logout` 统一处理:
- 清空 Keychain token。
- 清空账号快照。
- 重置账号上下文、权限上下文、景点上下文、全部 UIKit 导航栈和 Toast。
- `AppSession` 切换为 `loggedOut`
- 保留上次手机号、协议状态等非敏感偏好。
## Toast
全局 Toast 由 `RootViewController` 在切换 window 根控制器后挂载到 window业务页面通过 `showToast(...)``appServices.toastCenter.show(...)` 发出提示命令。
Toast 展示为顶部全宽横幅,背景使用不透明主色并延伸到屏幕顶部、左边和右边;文案居中展示,不提供关闭按钮,默认 2.2 秒后自动消失。连续展示新 Toast 时会覆盖旧文案并重新计时,旧的自动隐藏任务不会影响新的 Toast。
## 导航规则
主界面使用 `MainTabBarController` 作为 window 根控制器(登录后),每个 Tab 内部由独立的 `TabNavigationController``UINavigationController`)包裹。`UINavigationController.viewControllers` 是页面路径的唯一事实源,普通 Tab 切换保留该 Tab 的原有栈。
Tab 根页面显示底部 TabBar。通过 `AppRoute` push 到子页面时,会根据 `AppRoute.hidesTabBarWhenPushed` 统一隐藏 TabBar。
首页根页面使用自定义顶部栏,因此 `TabNavigationController` 会在展示 `HomeViewController` 时隐藏系统导航栏push 到其他业务子页面或从子页面返回时,由导航栈统一按栈顶页面恢复系统导航栏显隐,避免首页隐藏状态影响子页面标题栏和返回按钮。
跨 Tab、推送通知、UI Test 直达和全局扫码都通过 `AppNavigator` 处理。`AppNavigationPolicy.currentStack` 沿当前栈 push`.tabRoot` 切到目标 Tab 并先回到 root`.preserveTab` 只切换 Tab 并保留原栈。新增页面时优先扩展 `AppRoute` / `HomeRoute` / `ProfileRoute` / `OrdersRoute`,并在 `AppRouteViewControllerFactory` 注册对应 ViewController。