Refactor AppServices into assembly, feature facades, and lifecycle coordinator.
Extract dependency wiring and session lifecycle from the container and RootViewController, split NetworkBundle by domain, and add module-level feature services while keeping flat accessors for compatibility. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -13,7 +13,10 @@ App 模块负责应用入口、根视图切换、全局状态注入、主导航
|
||||
## 核心对象
|
||||
|
||||
- `AppDelegate` / `SceneDelegate`:UIKit 应用入口;`SceneDelegate` 挂载 `RootViewController` 并持有其强引用。
|
||||
- `AppServices`:全局依赖容器(Composition Root),等价于参考工程 SwiftUI `RootView` 中的 Environment 注入;内部按 Session / Context / Network / UI / Runtime 分组,对外仍保留扁平属性访问。
|
||||
- `AppServices`:全局依赖容器(Composition Root),等价于参考工程 SwiftUI `RootView` 中的 Environment 注入;自身只持有 Session / Context / Network / UI / Runtime 分组和导航器。
|
||||
- `AppServicesAssembly`:负责创建生产或测试依赖图,避免装配细节堆在容器本体中。
|
||||
- `AppFeatureServices`:按业务域提供模块级 Facade,避免页面散取全局 API、Context、Toast、Loading 和运行时依赖。
|
||||
- `AppSessionLifecycleCoordinator`:处理冷启动恢复、登录后景点刷新、登出清理和推送授权;`RootViewController` 只负责根控制器切换。
|
||||
- `RootViewController`:监听 `AppSession.phase`,未登录时在自身上展示登录/恢复页,登录后将 `MainTabBarController` 设为 window 根控制器,并挂载全局 Toast / Loading。
|
||||
- `AppSession`:保存认证阶段和正式 token,只负责登录态,不承载业务资料。
|
||||
- `AccountContext`:保存当前账号资料、景区作用域和门店作用域。
|
||||
@ -27,7 +30,19 @@ App 模块负责应用入口、根视图切换、全局状态注入、主导航
|
||||
|
||||
## AppServices 依赖约定
|
||||
|
||||
`AppServices.shared` 是唯一 Composition Root,集中创建 `APIClient`、各业务 API 与全局 Context,并绑定 token provider。
|
||||
`AppServices.shared` 是唯一 Composition Root,通过 `AppServicesAssembly` 集中创建 `APIClient`、各业务 API 与全局 Context,并绑定 token provider。
|
||||
|
||||
`AppServices` 本体保持轻量:只保存已装配的依赖分组、绑定跨分组关系和暴露 App 生命周期入口。兼容旧调用的扁平属性统一放在 `AppServices+Accessors.swift`,新增复杂业务不得继续塞进容器本体。
|
||||
|
||||
`NetworkBundle` 内部按业务域拆分为 Account / Commerce / Operation / Content 四组,所有 API 仍共享同一 `APIClient`。旧的 `services.ordersAPI`、`services.profileAPI` 等扁平访问器继续保留,仅作为兼容层。
|
||||
|
||||
新增页面优先通过模块 Facade 获取依赖:
|
||||
|
||||
- `ordersFeatureServices`:订单 API、景区/门店/角色、订单导航、Toast / Loading。
|
||||
- `profileFeatureServices`:登录、资料、账号上下文和退出登录清理。
|
||||
- `homeFeatureServices`:首页菜单、账号权限上下文和首页导航。
|
||||
- `assetsFeatureServices`:资产 API、OSS 上传、当前景区和云传记录仓库。
|
||||
- `queueFeatureServices`:排队 API、用户/景区/景点上下文和排队运行时。
|
||||
|
||||
### 何时通过 `init(services:)` 注入
|
||||
|
||||
@ -56,16 +71,17 @@ ViewModel **不持有** `AppServices`。View 从容器取出具体依赖,在 `
|
||||
|
||||
- `PushNotificationManager.shared`:`AppDelegate` 回调桥
|
||||
- `CloudTransferStore.shared`:跨页面云传任务状态
|
||||
- `ScenicQueueRuntime.shared`:跨页面排队轮询、后台任务和语音播报运行时
|
||||
- 第三方 SDK(如 `AMapServices`)
|
||||
|
||||
`AppSession`、`AccountContext`、各 `*API` 等**不应**拆成多个 `.shared`,须由 `AppServices` 统一创建,并在登出时由 `RootViewController` 统一 reset。
|
||||
`AppSession`、`AccountContext`、各 `*API`、`AppNavigator`、持久化 Store 等**不应**拆成多个 `.shared`,须由 `AppServices` 统一创建,并在登出时由 `AppSessionLifecycleCoordinator` 统一 reset。
|
||||
|
||||
## 启动流程
|
||||
|
||||
1. `AppDelegate` 创建 `RootViewController(services: .shared)`。
|
||||
2. `AppServices` 初始化共享依赖(`NetworkBundle` 内共享同一 `APIClient`)。
|
||||
2. `AppServices` 初始化共享依赖(`NetworkBundle` 子分组内共享同一 `APIClient`)。
|
||||
3. `APIClient` 绑定 `AppSession.token` 作为默认 token provider。
|
||||
4. `SessionBootstrapper.restore` 尝试从 Keychain 读取正式 token。
|
||||
4. `AppSessionLifecycleCoordinator` 调用 `SessionBootstrapper.restore` 尝试从 Keychain 读取正式 token。
|
||||
5. 无 token 时保持 `loggedOut`,展示 `LoginViewController`。
|
||||
6. 有 token 时进入 `restoring`,先恢复本地账号快照。
|
||||
7. `AccountContextLoader` 并行请求用户资料和角色权限,再补全景区与门店。
|
||||
|
||||
139
suixinkan_ios/App/AppFeatureServices.swift
Normal file
139
suixinkan_ios/App/AppFeatureServices.swift
Normal file
@ -0,0 +1,139 @@
|
||||
//
|
||||
// AppFeatureServices.swift
|
||||
// suixinkan
|
||||
//
|
||||
// Created by Codex on 2026/6/29.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@MainActor
|
||||
/// 订单模块依赖门面,集中提供订单页面常用 API、上下文和导航反馈能力。
|
||||
struct OrdersFeatureServices {
|
||||
private let services: AppServices
|
||||
|
||||
/// 初始化订单模块依赖门面。
|
||||
init(services: AppServices) {
|
||||
self.services = services
|
||||
}
|
||||
|
||||
var api: OrdersAPI { services.ordersAPI }
|
||||
var currentScenicId: Int? { services.currentScenicId }
|
||||
var currentStoreId: Int? { services.accountContext.currentStore?.id }
|
||||
var currentRoleId: Int? { services.permissionContext.currentRole?.id }
|
||||
var navigator: AppNavigator { services.appNavigator }
|
||||
var toastCenter: ToastCenter { services.toastCenter }
|
||||
var globalLoading: GlobalLoadingCenter { services.globalLoading }
|
||||
}
|
||||
|
||||
@MainActor
|
||||
/// 个人中心与认证模块依赖门面,集中提供账号资料、登录协调和退出登录能力。
|
||||
struct ProfileFeatureServices {
|
||||
private let services: AppServices
|
||||
|
||||
/// 初始化个人中心模块依赖门面。
|
||||
init(services: AppServices) {
|
||||
self.services = services
|
||||
}
|
||||
|
||||
var authAPI: AuthAPI { services.authAPI }
|
||||
var profileAPI: ProfileAPI { services.profileAPI }
|
||||
var accountContextAPI: AccountContextAPI { services.accountContextAPI }
|
||||
var uploader: OSSUploadService { services.ossUploadService }
|
||||
var appSession: AppSession { services.appSession }
|
||||
var accountContext: AccountContext { services.accountContext }
|
||||
var permissionContext: PermissionContext { services.permissionContext }
|
||||
var scenicSpotContext: ScenicSpotContext { services.scenicSpotContext }
|
||||
var authSessionCoordinator: AuthSessionCoordinator { services.authSessionCoordinator }
|
||||
var currentScenicId: Int? { services.currentScenicId }
|
||||
var navigator: AppNavigator { services.appNavigator }
|
||||
var toastCenter: ToastCenter { services.toastCenter }
|
||||
var globalLoading: GlobalLoadingCenter { services.globalLoading }
|
||||
|
||||
/// 执行完整退出登录,并停止跨登录态的排队运行时。
|
||||
func logout() {
|
||||
authSessionCoordinator.logout(
|
||||
appSession: appSession,
|
||||
accountContext: accountContext,
|
||||
permissionContext: permissionContext,
|
||||
scenicSpotContext: scenicSpotContext,
|
||||
appNavigator: navigator,
|
||||
toastCenter: toastCenter
|
||||
)
|
||||
services.scenicQueueRuntime.stop()
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
/// 首页模块依赖门面,集中提供首页菜单、账号权限上下文和首页导航能力。
|
||||
struct HomeFeatureServices {
|
||||
private let services: AppServices
|
||||
|
||||
/// 初始化首页模块依赖门面。
|
||||
init(services: AppServices) {
|
||||
self.services = services
|
||||
}
|
||||
|
||||
var accountContext: AccountContext { services.accountContext }
|
||||
var permissionContext: PermissionContext { services.permissionContext }
|
||||
var commonMenuStore: HomeCommonMenuStore { HomeCommonMenuStore() }
|
||||
var navigator: AppNavigator { services.appNavigator }
|
||||
var toastCenter: ToastCenter { services.toastCenter }
|
||||
}
|
||||
|
||||
@MainActor
|
||||
/// 资产模块依赖门面,集中提供素材云盘 API、上传服务、景区上下文和传输状态仓库。
|
||||
struct AssetsFeatureServices {
|
||||
private let services: AppServices
|
||||
|
||||
/// 初始化资产模块依赖门面。
|
||||
init(services: AppServices) {
|
||||
self.services = services
|
||||
}
|
||||
|
||||
var api: AssetsAPI { services.assetsAPI }
|
||||
var uploader: OSSUploadService { services.ossUploadService }
|
||||
var currentScenicId: Int? { services.currentScenicId }
|
||||
var transferStore: CloudTransferStore { CloudTransferStore.shared }
|
||||
var toastCenter: ToastCenter { services.toastCenter }
|
||||
var globalLoading: GlobalLoadingCenter { services.globalLoading }
|
||||
}
|
||||
|
||||
@MainActor
|
||||
/// 排队模块依赖门面,集中提供排队 API、账号景区上下文、景点上下文和排队运行时。
|
||||
struct QueueFeatureServices {
|
||||
private let services: AppServices
|
||||
|
||||
/// 初始化排队模块依赖门面。
|
||||
init(services: AppServices) {
|
||||
self.services = services
|
||||
}
|
||||
|
||||
var api: ScenicQueueAPI { services.scenicQueueAPI }
|
||||
var currentScenicId: Int? { services.currentScenicId }
|
||||
var userId: String? { services.userId }
|
||||
var scenicSpotContext: ScenicSpotContext { services.scenicSpotContext }
|
||||
var runtime: ScenicQueueRuntime { ScenicQueueRuntime.shared }
|
||||
var toastCenter: ToastCenter { services.toastCenter }
|
||||
var globalLoading: GlobalLoadingCenter { services.globalLoading }
|
||||
|
||||
/// 使用当前账号和景区上下文刷新 App 级排队运行时。
|
||||
func updateRuntime(scenePhase: AppScenePhase) {
|
||||
runtime.update(
|
||||
api: api,
|
||||
userId: userId,
|
||||
scenicId: currentScenicId,
|
||||
scenePhase: scenePhase
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
/// `AppServices` 的模块依赖门面访问器,供新代码按功能域读取依赖。
|
||||
extension AppServices {
|
||||
var ordersFeatureServices: OrdersFeatureServices { OrdersFeatureServices(services: self) }
|
||||
var profileFeatureServices: ProfileFeatureServices { ProfileFeatureServices(services: self) }
|
||||
var homeFeatureServices: HomeFeatureServices { HomeFeatureServices(services: self) }
|
||||
var assetsFeatureServices: AssetsFeatureServices { AssetsFeatureServices(services: self) }
|
||||
var queueFeatureServices: QueueFeatureServices { QueueFeatureServices(services: self) }
|
||||
}
|
||||
64
suixinkan_ios/App/AppServices+Accessors.swift
Normal file
64
suixinkan_ios/App/AppServices+Accessors.swift
Normal file
@ -0,0 +1,64 @@
|
||||
//
|
||||
// AppServices+Accessors.swift
|
||||
// suixinkan
|
||||
//
|
||||
// Created by Codex on 2026/6/29.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@MainActor
|
||||
/// `AppServices` 兼容访问器,供既有页面继续按扁平属性读取依赖。
|
||||
extension AppServices {
|
||||
var appSession: AppSession { session.appSession }
|
||||
var accountContext: AccountContext { context.accountContext }
|
||||
var permissionContext: PermissionContext { context.permissionContext }
|
||||
var scenicSpotContext: ScenicSpotContext { context.scenicSpotContext }
|
||||
var toastCenter: ToastCenter { ui.toastCenter }
|
||||
var globalLoading: GlobalLoadingCenter { ui.globalLoading }
|
||||
var scenicQueueRuntime: ScenicQueueRuntime { runtime.scenicQueueRuntime }
|
||||
|
||||
var apiClient: APIClient { network.apiClient }
|
||||
var authAPI: AuthAPI { network.authAPI }
|
||||
var profileAPI: ProfileAPI { network.profileAPI }
|
||||
var uploadAPI: UploadAPI { network.uploadAPI }
|
||||
var ossUploadService: OSSUploadService { network.ossUploadService }
|
||||
var accountContextAPI: AccountContextAPI { network.accountContextAPI }
|
||||
var ordersAPI: OrdersAPI { network.ordersAPI }
|
||||
var statisticsAPI: StatisticsAPI { network.statisticsAPI }
|
||||
var paymentAPI: PaymentAPI { network.paymentAPI }
|
||||
var walletAPI: WalletAPI { network.walletAPI }
|
||||
var pushAPI: PushAPI { network.pushAPI }
|
||||
var scenicPermissionAPI: ScenicPermissionAPI { network.scenicPermissionAPI }
|
||||
var scenicSettlementAPI: ScenicSettlementAPI { network.scenicSettlementAPI }
|
||||
var messageCenterAPI: MessageCenterAPI { network.messageCenterAPI }
|
||||
var scenicQueueAPI: ScenicQueueAPI { network.scenicQueueAPI }
|
||||
var liveAPI: LiveAPI { network.liveAPI }
|
||||
var operatingAreaAPI: OperatingAreaAPI { network.operatingAreaAPI }
|
||||
var pilotCertificationAPI: PilotCertificationAPI { network.pilotCertificationAPI }
|
||||
var taskAPI: TaskAPI { network.taskAPI }
|
||||
var projectAPI: ProjectAPI { network.projectAPI }
|
||||
var scheduleAPI: ScheduleAPI { network.scheduleAPI }
|
||||
var inviteAPI: InviteAPI { network.inviteAPI }
|
||||
var assetsAPI: AssetsAPI { network.assetsAPI }
|
||||
var punchPointAPI: PunchPointAPI { network.punchPointAPI }
|
||||
var locationReportAPI: LocationReportAPI { network.locationReportAPI }
|
||||
|
||||
var authSessionCoordinator: AuthSessionCoordinator { session.authSessionCoordinator }
|
||||
var sessionBootstrapper: SessionBootstrapper { session.sessionBootstrapper }
|
||||
|
||||
/// 当前景区 ID。
|
||||
var currentScenicId: Int? {
|
||||
accountContext.currentScenic?.id
|
||||
}
|
||||
|
||||
/// 当前用户 staffId,用于钱包等接口。
|
||||
var staffId: Int? {
|
||||
Int(accountContext.profile?.userId ?? "")
|
||||
}
|
||||
|
||||
/// 当前用户 ID 字符串。
|
||||
var userId: String? {
|
||||
accountContext.profile?.userId
|
||||
}
|
||||
}
|
||||
@ -17,132 +17,39 @@ final class AppServices {
|
||||
let network: NetworkBundle
|
||||
let ui: UIFeedbackBundle
|
||||
let runtime: AppRuntimeBundle
|
||||
let appNavigator = AppNavigator()
|
||||
|
||||
private let tokenStore: SessionTokenStore
|
||||
private let snapshotStore: AccountSnapshotStore
|
||||
|
||||
// MARK: - 扁平访问(兼容现有调用方)
|
||||
|
||||
var appSession: AppSession { session.appSession }
|
||||
var accountContext: AccountContext { context.accountContext }
|
||||
var permissionContext: PermissionContext { context.permissionContext }
|
||||
var scenicSpotContext: ScenicSpotContext { context.scenicSpotContext }
|
||||
var toastCenter: ToastCenter { ui.toastCenter }
|
||||
var globalLoading: GlobalLoadingCenter { ui.globalLoading }
|
||||
var scenicQueueRuntime: ScenicQueueRuntime { runtime.scenicQueueRuntime }
|
||||
|
||||
var apiClient: APIClient { network.apiClient }
|
||||
var authAPI: AuthAPI { network.authAPI }
|
||||
var profileAPI: ProfileAPI { network.profileAPI }
|
||||
var uploadAPI: UploadAPI { network.uploadAPI }
|
||||
var ossUploadService: OSSUploadService { network.ossUploadService }
|
||||
var accountContextAPI: AccountContextAPI { network.accountContextAPI }
|
||||
var ordersAPI: OrdersAPI { network.ordersAPI }
|
||||
var statisticsAPI: StatisticsAPI { network.statisticsAPI }
|
||||
var paymentAPI: PaymentAPI { network.paymentAPI }
|
||||
var walletAPI: WalletAPI { network.walletAPI }
|
||||
var pushAPI: PushAPI { network.pushAPI }
|
||||
var scenicPermissionAPI: ScenicPermissionAPI { network.scenicPermissionAPI }
|
||||
var scenicSettlementAPI: ScenicSettlementAPI { network.scenicSettlementAPI }
|
||||
var messageCenterAPI: MessageCenterAPI { network.messageCenterAPI }
|
||||
var scenicQueueAPI: ScenicQueueAPI { network.scenicQueueAPI }
|
||||
var liveAPI: LiveAPI { network.liveAPI }
|
||||
var operatingAreaAPI: OperatingAreaAPI { network.operatingAreaAPI }
|
||||
var pilotCertificationAPI: PilotCertificationAPI { network.pilotCertificationAPI }
|
||||
var taskAPI: TaskAPI { network.taskAPI }
|
||||
var projectAPI: ProjectAPI { network.projectAPI }
|
||||
var scheduleAPI: ScheduleAPI { network.scheduleAPI }
|
||||
var inviteAPI: InviteAPI { network.inviteAPI }
|
||||
var assetsAPI: AssetsAPI { network.assetsAPI }
|
||||
var punchPointAPI: PunchPointAPI { network.punchPointAPI }
|
||||
var locationReportAPI: LocationReportAPI { network.locationReportAPI }
|
||||
|
||||
var authSessionCoordinator: AuthSessionCoordinator { session.authSessionCoordinator }
|
||||
var sessionBootstrapper: SessionBootstrapper { session.sessionBootstrapper }
|
||||
|
||||
/// 当前景区 ID。
|
||||
var currentScenicId: Int? {
|
||||
accountContext.currentScenic?.id
|
||||
}
|
||||
|
||||
/// 当前用户 staffId,用于钱包等接口。
|
||||
var staffId: Int? {
|
||||
Int(accountContext.profile?.userId ?? "")
|
||||
}
|
||||
|
||||
/// 当前用户 ID 字符串。
|
||||
var userId: String? {
|
||||
accountContext.profile?.userId
|
||||
}
|
||||
let appNavigator: AppNavigator
|
||||
|
||||
/// 生产环境默认初始化。
|
||||
private init() {
|
||||
let tokenStore = SessionTokenStore()
|
||||
let snapshotStore = AccountSnapshotStore()
|
||||
let preferencesStore = AppPreferencesStore()
|
||||
let apiClient = APIClient()
|
||||
|
||||
self.tokenStore = tokenStore
|
||||
self.snapshotStore = snapshotStore
|
||||
context = AppContextBundle(
|
||||
accountContext: AccountContext(),
|
||||
permissionContext: PermissionContext(),
|
||||
scenicSpotContext: ScenicSpotContext()
|
||||
)
|
||||
network = NetworkBundle(apiClient: apiClient)
|
||||
ui = UIFeedbackBundle(
|
||||
toastCenter: ToastCenter(),
|
||||
globalLoading: GlobalLoadingCenter()
|
||||
)
|
||||
runtime = AppRuntimeBundle(scenicQueueRuntime: ScenicQueueRuntime())
|
||||
session = SessionBundle(
|
||||
appSession: AppSession(),
|
||||
authSessionCoordinator: AuthSessionCoordinator(
|
||||
tokenStore: tokenStore,
|
||||
snapshotStore: snapshotStore,
|
||||
preferencesStore: preferencesStore
|
||||
),
|
||||
sessionBootstrapper: SessionBootstrapper(
|
||||
tokenStore: tokenStore,
|
||||
snapshotStore: snapshotStore
|
||||
)
|
||||
)
|
||||
bindAuthTokenProvider()
|
||||
private convenience init() {
|
||||
self.init(assembly: AppServicesAssembly())
|
||||
}
|
||||
|
||||
/// 测试专用初始化,允许注入 mock `APIClient` 与存储替身。
|
||||
init(
|
||||
convenience init(
|
||||
apiClient: APIClient,
|
||||
tokenStore: SessionTokenStore = SessionTokenStore(),
|
||||
snapshotStore: AccountSnapshotStore = AccountSnapshotStore(),
|
||||
preferencesStore: AppPreferencesStore = AppPreferencesStore()
|
||||
) {
|
||||
self.tokenStore = tokenStore
|
||||
self.snapshotStore = snapshotStore
|
||||
context = AppContextBundle(
|
||||
accountContext: AccountContext(),
|
||||
permissionContext: PermissionContext(),
|
||||
scenicSpotContext: ScenicSpotContext()
|
||||
)
|
||||
network = NetworkBundle(apiClient: apiClient)
|
||||
ui = UIFeedbackBundle(
|
||||
toastCenter: ToastCenter(),
|
||||
globalLoading: GlobalLoadingCenter()
|
||||
)
|
||||
runtime = AppRuntimeBundle(scenicQueueRuntime: ScenicQueueRuntime())
|
||||
session = SessionBundle(
|
||||
appSession: AppSession(),
|
||||
authSessionCoordinator: AuthSessionCoordinator(
|
||||
self.init(
|
||||
assembly: AppServicesAssembly(
|
||||
apiClient: apiClient,
|
||||
tokenStore: tokenStore,
|
||||
snapshotStore: snapshotStore,
|
||||
preferencesStore: preferencesStore
|
||||
),
|
||||
sessionBootstrapper: SessionBootstrapper(
|
||||
tokenStore: tokenStore,
|
||||
snapshotStore: snapshotStore
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/// 使用装配器完成容器初始化并建立跨 bundle 绑定。
|
||||
private init(assembly: AppServicesAssembly) {
|
||||
let dependencies = assembly.makeDependencies()
|
||||
session = dependencies.session
|
||||
context = dependencies.context
|
||||
network = dependencies.network
|
||||
ui = dependencies.ui
|
||||
runtime = dependencies.runtime
|
||||
appNavigator = dependencies.appNavigator
|
||||
bindAuthTokenProvider()
|
||||
}
|
||||
|
||||
|
||||
84
suixinkan_ios/App/AppServicesAssembly.swift
Normal file
84
suixinkan_ios/App/AppServicesAssembly.swift
Normal file
@ -0,0 +1,84 @@
|
||||
//
|
||||
// AppServicesAssembly.swift
|
||||
// suixinkan
|
||||
//
|
||||
// Created by Codex on 2026/6/29.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@MainActor
|
||||
/// `AppServices` 的装配输入,集中保存生产与测试环境可替换的底层依赖。
|
||||
struct AppServicesAssembly {
|
||||
private let apiClient: APIClient
|
||||
private let tokenStore: SessionTokenStore
|
||||
private let snapshotStore: AccountSnapshotStore
|
||||
private let preferencesStore: AppPreferencesStore
|
||||
|
||||
/// 初始化生产装配器。
|
||||
init() {
|
||||
apiClient = APIClient()
|
||||
tokenStore = SessionTokenStore()
|
||||
snapshotStore = AccountSnapshotStore()
|
||||
preferencesStore = AppPreferencesStore()
|
||||
}
|
||||
|
||||
/// 初始化测试装配器,允许传入网络与存储替身。
|
||||
init(
|
||||
apiClient: APIClient,
|
||||
tokenStore: SessionTokenStore,
|
||||
snapshotStore: AccountSnapshotStore,
|
||||
preferencesStore: AppPreferencesStore
|
||||
) {
|
||||
self.apiClient = apiClient
|
||||
self.tokenStore = tokenStore
|
||||
self.snapshotStore = snapshotStore
|
||||
self.preferencesStore = preferencesStore
|
||||
}
|
||||
|
||||
/// 创建 `AppServices` 所需的完整依赖图。
|
||||
func makeDependencies() -> AppServicesDependencies {
|
||||
AppServicesDependencies(
|
||||
session: makeSessionBundle(),
|
||||
context: AppContextBundle(
|
||||
accountContext: AccountContext(),
|
||||
permissionContext: PermissionContext(),
|
||||
scenicSpotContext: ScenicSpotContext()
|
||||
),
|
||||
network: NetworkBundle(apiClient: apiClient),
|
||||
ui: UIFeedbackBundle(
|
||||
toastCenter: ToastCenter(),
|
||||
globalLoading: GlobalLoadingCenter()
|
||||
),
|
||||
runtime: AppRuntimeBundle(scenicQueueRuntime: ScenicQueueRuntime.shared),
|
||||
appNavigator: AppNavigator()
|
||||
)
|
||||
}
|
||||
|
||||
/// 创建登录恢复与退出登录相关依赖。
|
||||
private func makeSessionBundle() -> SessionBundle {
|
||||
SessionBundle(
|
||||
appSession: AppSession(),
|
||||
authSessionCoordinator: AuthSessionCoordinator(
|
||||
tokenStore: tokenStore,
|
||||
snapshotStore: snapshotStore,
|
||||
preferencesStore: preferencesStore
|
||||
),
|
||||
sessionBootstrapper: SessionBootstrapper(
|
||||
tokenStore: tokenStore,
|
||||
snapshotStore: snapshotStore
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
/// 已装配好的应用级依赖集合,供 `AppServices` 直接持有。
|
||||
struct AppServicesDependencies {
|
||||
let session: SessionBundle
|
||||
let context: AppContextBundle
|
||||
let network: NetworkBundle
|
||||
let ui: UIFeedbackBundle
|
||||
let runtime: AppRuntimeBundle
|
||||
let appNavigator: AppNavigator
|
||||
}
|
||||
@ -27,61 +27,130 @@ struct AppContextBundle {
|
||||
/// 网络层依赖分组,共享同一 `APIClient` 与 token 链路。
|
||||
struct NetworkBundle {
|
||||
let apiClient: APIClient
|
||||
let account: AccountNetworkBundle
|
||||
let commerce: CommerceNetworkBundle
|
||||
let operation: OperationNetworkBundle
|
||||
let content: ContentNetworkBundle
|
||||
|
||||
var authAPI: AuthAPI { account.authAPI }
|
||||
var profileAPI: ProfileAPI { account.profileAPI }
|
||||
var accountContextAPI: AccountContextAPI { account.accountContextAPI }
|
||||
var pushAPI: PushAPI { account.pushAPI }
|
||||
var ordersAPI: OrdersAPI { commerce.ordersAPI }
|
||||
var paymentAPI: PaymentAPI { commerce.paymentAPI }
|
||||
var walletAPI: WalletAPI { commerce.walletAPI }
|
||||
var inviteAPI: InviteAPI { commerce.inviteAPI }
|
||||
var scenicSettlementAPI: ScenicSettlementAPI { commerce.scenicSettlementAPI }
|
||||
var statisticsAPI: StatisticsAPI { operation.statisticsAPI }
|
||||
var scenicPermissionAPI: ScenicPermissionAPI { operation.scenicPermissionAPI }
|
||||
var scenicQueueAPI: ScenicQueueAPI { operation.scenicQueueAPI }
|
||||
var operatingAreaAPI: OperatingAreaAPI { operation.operatingAreaAPI }
|
||||
var pilotCertificationAPI: PilotCertificationAPI { operation.pilotCertificationAPI }
|
||||
var taskAPI: TaskAPI { operation.taskAPI }
|
||||
var scheduleAPI: ScheduleAPI { operation.scheduleAPI }
|
||||
var punchPointAPI: PunchPointAPI { operation.punchPointAPI }
|
||||
var locationReportAPI: LocationReportAPI { operation.locationReportAPI }
|
||||
var uploadAPI: UploadAPI { content.uploadAPI }
|
||||
var ossUploadService: OSSUploadService { content.ossUploadService }
|
||||
var messageCenterAPI: MessageCenterAPI { content.messageCenterAPI }
|
||||
var liveAPI: LiveAPI { content.liveAPI }
|
||||
var projectAPI: ProjectAPI { content.projectAPI }
|
||||
var assetsAPI: AssetsAPI { content.assetsAPI }
|
||||
|
||||
/// 基于共享 `APIClient` 创建全部业务 API 分组。
|
||||
init(apiClient: APIClient) {
|
||||
self.apiClient = apiClient
|
||||
account = AccountNetworkBundle(apiClient: apiClient)
|
||||
commerce = CommerceNetworkBundle(apiClient: apiClient)
|
||||
operation = OperationNetworkBundle(apiClient: apiClient)
|
||||
content = ContentNetworkBundle(apiClient: apiClient)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
/// 账号、登录、资料和推送相关网络依赖。
|
||||
struct AccountNetworkBundle {
|
||||
let authAPI: AuthAPI
|
||||
let profileAPI: ProfileAPI
|
||||
let uploadAPI: UploadAPI
|
||||
let ossUploadService: OSSUploadService
|
||||
let accountContextAPI: AccountContextAPI
|
||||
let pushAPI: PushAPI
|
||||
|
||||
/// 基于共享 `APIClient` 创建账号网络依赖。
|
||||
init(apiClient: APIClient) {
|
||||
authAPI = AuthAPI(client: apiClient)
|
||||
profileAPI = ProfileAPI(client: apiClient)
|
||||
accountContextAPI = AccountContextAPI(client: apiClient)
|
||||
pushAPI = PushAPI(client: apiClient)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
/// 订单、收款、钱包、邀请和结算相关网络依赖。
|
||||
struct CommerceNetworkBundle {
|
||||
let ordersAPI: OrdersAPI
|
||||
let statisticsAPI: StatisticsAPI
|
||||
let paymentAPI: PaymentAPI
|
||||
let walletAPI: WalletAPI
|
||||
let pushAPI: PushAPI
|
||||
let scenicPermissionAPI: ScenicPermissionAPI
|
||||
let inviteAPI: InviteAPI
|
||||
let scenicSettlementAPI: ScenicSettlementAPI
|
||||
let messageCenterAPI: MessageCenterAPI
|
||||
|
||||
/// 基于共享 `APIClient` 创建交易网络依赖。
|
||||
init(apiClient: APIClient) {
|
||||
ordersAPI = OrdersAPI(client: apiClient)
|
||||
paymentAPI = PaymentAPI(client: apiClient)
|
||||
walletAPI = WalletAPI(client: apiClient)
|
||||
inviteAPI = InviteAPI(client: apiClient)
|
||||
scenicSettlementAPI = ScenicSettlementAPI(client: apiClient)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
/// 运营管理、权限、排队、任务、排期和位置上报相关网络依赖。
|
||||
struct OperationNetworkBundle {
|
||||
let statisticsAPI: StatisticsAPI
|
||||
let scenicPermissionAPI: ScenicPermissionAPI
|
||||
let scenicQueueAPI: ScenicQueueAPI
|
||||
let liveAPI: LiveAPI
|
||||
let operatingAreaAPI: OperatingAreaAPI
|
||||
let pilotCertificationAPI: PilotCertificationAPI
|
||||
let taskAPI: TaskAPI
|
||||
let projectAPI: ProjectAPI
|
||||
let scheduleAPI: ScheduleAPI
|
||||
let inviteAPI: InviteAPI
|
||||
let assetsAPI: AssetsAPI
|
||||
let punchPointAPI: PunchPointAPI
|
||||
let locationReportAPI: LocationReportAPI
|
||||
|
||||
/// 基于共享 `APIClient` 创建全部业务 API。
|
||||
/// 基于共享 `APIClient` 创建运营网络依赖。
|
||||
init(apiClient: APIClient) {
|
||||
self.apiClient = apiClient
|
||||
authAPI = AuthAPI(client: apiClient)
|
||||
profileAPI = ProfileAPI(client: apiClient)
|
||||
uploadAPI = UploadAPI(client: apiClient)
|
||||
ossUploadService = OSSUploadService(configService: uploadAPI)
|
||||
accountContextAPI = AccountContextAPI(client: apiClient)
|
||||
ordersAPI = OrdersAPI(client: apiClient)
|
||||
statisticsAPI = StatisticsAPI(client: apiClient)
|
||||
paymentAPI = PaymentAPI(client: apiClient)
|
||||
walletAPI = WalletAPI(client: apiClient)
|
||||
pushAPI = PushAPI(client: apiClient)
|
||||
scenicPermissionAPI = ScenicPermissionAPI(client: apiClient)
|
||||
scenicSettlementAPI = ScenicSettlementAPI(client: apiClient)
|
||||
messageCenterAPI = MessageCenterAPI(client: apiClient)
|
||||
scenicQueueAPI = ScenicQueueAPI(client: apiClient)
|
||||
liveAPI = LiveAPI(client: apiClient)
|
||||
operatingAreaAPI = OperatingAreaAPI(client: apiClient)
|
||||
pilotCertificationAPI = PilotCertificationAPI(client: apiClient)
|
||||
taskAPI = TaskAPI(client: apiClient)
|
||||
projectAPI = ProjectAPI(client: apiClient)
|
||||
scheduleAPI = ScheduleAPI(client: apiClient)
|
||||
inviteAPI = InviteAPI(client: apiClient)
|
||||
assetsAPI = AssetsAPI(client: apiClient)
|
||||
punchPointAPI = PunchPointAPI(client: apiClient)
|
||||
locationReportAPI = LocationReportAPI(client: apiClient)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
/// 内容、素材、直播、项目和消息中心相关网络依赖。
|
||||
struct ContentNetworkBundle {
|
||||
let uploadAPI: UploadAPI
|
||||
let ossUploadService: OSSUploadService
|
||||
let messageCenterAPI: MessageCenterAPI
|
||||
let liveAPI: LiveAPI
|
||||
let projectAPI: ProjectAPI
|
||||
let assetsAPI: AssetsAPI
|
||||
|
||||
/// 基于共享 `APIClient` 创建内容网络依赖。
|
||||
init(apiClient: APIClient) {
|
||||
uploadAPI = UploadAPI(client: apiClient)
|
||||
ossUploadService = OSSUploadService(configService: uploadAPI)
|
||||
messageCenterAPI = MessageCenterAPI(client: apiClient)
|
||||
liveAPI = LiveAPI(client: apiClient)
|
||||
projectAPI = ProjectAPI(client: apiClient)
|
||||
assetsAPI = AssetsAPI(client: apiClient)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
/// 全局 UI 反馈(Toast、Loading)分组。
|
||||
struct UIFeedbackBundle {
|
||||
|
||||
84
suixinkan_ios/App/AppSessionLifecycleCoordinator.swift
Normal file
84
suixinkan_ios/App/AppSessionLifecycleCoordinator.swift
Normal file
@ -0,0 +1,84 @@
|
||||
//
|
||||
// AppSessionLifecycleCoordinator.swift
|
||||
// suixinkan
|
||||
//
|
||||
// Created by Codex on 2026/6/29.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@MainActor
|
||||
/// 应用会话生命周期协调器,负责登录恢复、登录后上下文刷新、登出清理和推送注册。
|
||||
final class AppSessionLifecycleCoordinator {
|
||||
private let services: AppServices
|
||||
|
||||
/// 初始化会话生命周期协调器。
|
||||
init(services: AppServices) {
|
||||
self.services = services
|
||||
}
|
||||
|
||||
/// 配置系统推送入口。
|
||||
func configurePushNotifications() {
|
||||
services.configurePushNotifications()
|
||||
}
|
||||
|
||||
/// 冷启动时恢复本地会话,并在已登录时申请推送权限。
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
requestPushAuthorizationIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
/// 登录阶段变化后的业务状态处理,根视图切换由调用方先完成。
|
||||
func handleSessionPhaseChange() {
|
||||
switch services.appSession.phase {
|
||||
case .loggedIn:
|
||||
requestPushAuthorizationIfNeeded()
|
||||
Task { await reloadScenicSpotContextIfNeeded() }
|
||||
case .loggedOut:
|
||||
resetStateAfterLogout()
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
/// 登出后重置账号、权限、路由与排队运行时,避免残留旧状态。
|
||||
private func resetStateAfterLogout() {
|
||||
services.accountContext.reset()
|
||||
services.permissionContext.reset()
|
||||
services.scenicSpotContext.reset()
|
||||
services.appNavigator.resetAllStacks()
|
||||
services.appNavigator.detach()
|
||||
services.toastCenter.dismiss()
|
||||
services.scenicQueueRuntime.stop()
|
||||
}
|
||||
|
||||
/// 非 UI Test 环境下申请推送权限并注册 APNs。
|
||||
private func requestPushAuthorizationIfNeeded() {
|
||||
guard services.appSession.isLoggedIn, !AppUITestLaunchState.isRunningUITests else { return }
|
||||
PushNotificationManager.shared.requestAuthorizationAndRegister()
|
||||
}
|
||||
}
|
||||
@ -11,11 +11,13 @@ import UIKit
|
||||
final class RootViewController: UIViewController {
|
||||
|
||||
private let services: AppServices
|
||||
private let lifecycleCoordinator: AppSessionLifecycleCoordinator
|
||||
private var currentAuthChild: UIViewController?
|
||||
|
||||
/// 初始化实例。
|
||||
init(services: AppServices = .shared) {
|
||||
self.services = services
|
||||
lifecycleCoordinator = AppSessionLifecycleCoordinator(services: services)
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@ -34,64 +36,14 @@ final class RootViewController: UIViewController {
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
lifecycleCoordinator.bootstrapSession()
|
||||
lifecycleCoordinator.configurePushNotifications()
|
||||
}
|
||||
|
||||
/// 登录阶段变化时切换 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
|
||||
)
|
||||
lifecycleCoordinator.handleSessionPhaseChange()
|
||||
}
|
||||
|
||||
/// 按登录阶段切换 window 根控制器。
|
||||
|
||||
Reference in New Issue
Block a user