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:
2026-06-29 09:32:44 +08:00
parent 1a6d4a1791
commit 0d8f97417b
11 changed files with 686 additions and 197 deletions

View File

@ -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` 并行请求用户资料和角色权限,再补全景区与门店。

View 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) }
}

View 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
}
}

View File

@ -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()
}

View 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
}

View File

@ -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 ToastLoading
struct UIFeedbackBundle {

View 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()
}
}

View File

@ -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

View File

@ -18,6 +18,7 @@ enum AppScenePhase {
@MainActor
///
final class ScenicQueueRuntime {
static let shared = ScenicQueueRuntime()
var onChange: (() -> Void)?
private(set) var isMonitoring = false { didSet { onChange?() } }