Refactor AppServices into grouped bundles and document DI conventions.
Centralize dependency wiring with Session/Context/Network/UI/Runtime bundles, unify leaf ViewControllers on appServices, and add a test initializer with AppServicesTests. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -2,18 +2,19 @@
|
||||
|
||||
## 模块职责
|
||||
|
||||
App 模块负责应用入口、根视图切换、全局状态注入、主导航状态、登录态恢复和全局 Toast。
|
||||
App 模块负责应用入口、根视图切换、全局状态注入、主导航状态、登录态恢复和全局 Toast / Loading。
|
||||
|
||||
该模块不直接处理具体页面业务,主要承担 App 级基础设施编排:
|
||||
- 根据登录状态展示登录页、恢复态或主 Tab。
|
||||
- 创建并注入全局状态和共享服务。
|
||||
- 管理每个 Tab 独立的 `NavigationStack` 路径。
|
||||
- 管理每个 Tab 独立的导航栈与 `AppRouter` 路径。
|
||||
- 协调登录成功、退出登录、冷启动恢复和本地缓存同步。
|
||||
|
||||
## 核心对象
|
||||
|
||||
- `suixinkanApp`:SwiftUI 应用入口,挂载 `RootView`。
|
||||
- `RootView`:创建 `AppSession`、`AccountContext`、`PermissionContext`、`ScenicSpotContext`、`AppRouter`、`ToastCenter`、`APIClient` 和业务 API,并注入 SwiftUI Environment。
|
||||
- `AppDelegate` / `SceneDelegate`:UIKit 应用入口,挂载 `RootViewController`。
|
||||
- `AppServices`:全局依赖容器(Composition Root),等价于参考工程 SwiftUI `RootView` 中的 Environment 注入;内部按 Session / Context / Network / UI / Runtime 分组,对外仍保留扁平属性访问。
|
||||
- `RootViewController`:根据 `AppSession.phase` 切换登录页、恢复占位或 `MainTabBarController`,并挂载全局 Toast / Loading。
|
||||
- `AppSession`:保存认证阶段和正式 token,只负责登录态,不承载业务资料。
|
||||
- `AccountContext`:保存当前账号资料、景区作用域和门店作用域。
|
||||
- `PermissionContext`:保存角色权限、当前角色和扁平化权限 URI。
|
||||
@ -24,16 +25,51 @@ App 模块负责应用入口、根视图切换、全局状态注入、主导航
|
||||
- `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:)`。
|
||||
|
||||
导航辅助(`UIKitAppNavigation`、`HomeMenuRouting`)在全局 push 场景可直接使用 `AppServices.shared`。
|
||||
|
||||
### 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. `suixinkanApp` 创建 `RootView`。
|
||||
2. `RootView` 初始化共享依赖,并把它们注入 Environment。
|
||||
1. `AppDelegate` 创建 `RootViewController(services: .shared)`。
|
||||
2. `AppServices` 初始化共享依赖(`NetworkBundle` 内共享同一 `APIClient`)。
|
||||
3. `APIClient` 绑定 `AppSession.token` 作为默认 token provider。
|
||||
4. `SessionBootstrapper.restore` 尝试从 Keychain 读取正式 token。
|
||||
5. 无 token 时保持 `loggedOut`,展示 `LoginView`。
|
||||
5. 无 token 时保持 `loggedOut`,展示 `LoginViewController`。
|
||||
6. 有 token 时进入 `restoring`,先恢复本地账号快照。
|
||||
7. `AccountContextLoader` 并行请求用户资料和角色权限,再补全景区与门店。
|
||||
8. 校验成功后进入 `loggedIn`,展示 `MainTabsView`。
|
||||
8. 校验成功后进入 `loggedIn`,展示 `MainTabBarController`。
|
||||
9. `ScenicSpotContext` 按当前景区懒加载景点/打卡点。
|
||||
10. 明确 token 失效时清空 token 和账号快照,回到登录页。
|
||||
11. 普通网络失败时保留本地登录态,使用账号快照进入主界面。
|
||||
@ -66,14 +102,14 @@ App 模块负责应用入口、根视图切换、全局状态注入、主导航
|
||||
|
||||
## Toast
|
||||
|
||||
全局 Toast 由 `RootView` 挂载在页面最上层,业务页面只调用 `toastCenter.show(...)` 发出提示命令。
|
||||
全局 Toast 由 `RootViewController` 在 `viewDidAppear` 时挂载到 window,业务页面通过 `showToast(...)` 或 `appServices.toastCenter.show(...)` 发出提示命令。
|
||||
|
||||
Toast 展示为顶部全宽横幅,背景使用不透明主色并延伸到屏幕顶部、左边和右边;文案居中展示,不提供关闭按钮,默认 2.2 秒后自动消失。连续展示新 Toast 时会覆盖旧文案并重新计时,旧的自动隐藏任务不会影响新的 Toast。
|
||||
|
||||
## 导航规则
|
||||
|
||||
主界面使用 `TabView`,每个 Tab 内部由单独的 `NavigationStack` 包裹。`AppRouter` 为每个 `AppTab` 持有独立 `RouterPath`,切换 Tab 不会丢失该 Tab 的内部导航路径。
|
||||
主界面使用 `MainTabBarController`,每个 Tab 内部由独立的 `TabNavigationController`(`UINavigationController`)包裹。`AppRouter` 为每个 `AppTab` 持有独立 `RouterPath`,切换 Tab 不会丢失该 Tab 的内部导航路径。
|
||||
|
||||
Tab 根页面显示底部 TabBar。通过 `AppRoute` push 到子页面时,`MainTabsView` 会根据 `AppRoute.hidesTabBarWhenPushed` 统一隐藏 TabBar,避免每个业务页面重复处理。
|
||||
Tab 根页面显示底部 TabBar。通过 `AppRoute` push 到子页面时,会根据 `AppRoute.hidesTabBarWhenPushed` 统一隐藏 TabBar。
|
||||
|
||||
当前路由枚举 `AppRoute` 仍以占位详情页为主,后续新增真实页面时应优先扩展 `AppRoute`,再由对应 Tab 的 `NavigationStack` 处理跳转。
|
||||
跨 Tab 跳转通过 `AppRouter.select` / `navigateHome` / `navigateProfile` 处理;Tab 内 push 通过 `UIKitAppNavigation` 或当前 Nav 栈完成。新增页面时优先扩展 `AppRoute` / `HomeRoute`,在 `AppRouteViewControllerFactory` 注册对应 ViewController。
|
||||
|
||||
Reference in New Issue
Block a user