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。
|
||||
|
||||
@ -12,46 +12,55 @@ import Foundation
|
||||
final class AppServices {
|
||||
static let shared = AppServices()
|
||||
|
||||
let appSession = AppSession()
|
||||
let accountContext = AccountContext()
|
||||
let permissionContext = PermissionContext()
|
||||
let scenicSpotContext = ScenicSpotContext()
|
||||
let session: SessionBundle
|
||||
let context: AppContextBundle
|
||||
let network: NetworkBundle
|
||||
let ui: UIFeedbackBundle
|
||||
let runtime: AppRuntimeBundle
|
||||
let appRouter = AppRouter()
|
||||
let toastCenter = ToastCenter()
|
||||
let globalLoading = GlobalLoadingCenter()
|
||||
let scenicQueueRuntime = ScenicQueueRuntime()
|
||||
|
||||
let apiClient: APIClient
|
||||
let authAPI: AuthAPI
|
||||
let profileAPI: ProfileAPI
|
||||
let uploadAPI: UploadAPI
|
||||
let ossUploadService: OSSUploadService
|
||||
let accountContextAPI: AccountContextAPI
|
||||
let ordersAPI: OrdersAPI
|
||||
let statisticsAPI: StatisticsAPI
|
||||
let paymentAPI: PaymentAPI
|
||||
let walletAPI: WalletAPI
|
||||
let pushAPI: PushAPI
|
||||
let scenicPermissionAPI: ScenicPermissionAPI
|
||||
let scenicSettlementAPI: ScenicSettlementAPI
|
||||
let messageCenterAPI: MessageCenterAPI
|
||||
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
|
||||
|
||||
let authSessionCoordinator: AuthSessionCoordinator
|
||||
let sessionBootstrapper: SessionBootstrapper
|
||||
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
|
||||
@ -67,50 +76,74 @@ final class AppServices {
|
||||
accountContext.profile?.userId
|
||||
}
|
||||
|
||||
/// 初始化实例。
|
||||
/// 生产环境默认初始化。
|
||||
private init() {
|
||||
let client = APIClient()
|
||||
let tokenStore = SessionTokenStore()
|
||||
let snapshotStore = AccountSnapshotStore()
|
||||
let preferencesStore = AppPreferencesStore()
|
||||
let apiClient = APIClient()
|
||||
|
||||
self.tokenStore = tokenStore
|
||||
self.snapshotStore = snapshotStore
|
||||
apiClient = client
|
||||
authAPI = AuthAPI(client: client)
|
||||
profileAPI = ProfileAPI(client: client)
|
||||
uploadAPI = UploadAPI(client: client)
|
||||
ossUploadService = OSSUploadService(configService: uploadAPI)
|
||||
accountContextAPI = AccountContextAPI(client: client)
|
||||
ordersAPI = OrdersAPI(client: client)
|
||||
statisticsAPI = StatisticsAPI(client: client)
|
||||
paymentAPI = PaymentAPI(client: client)
|
||||
walletAPI = WalletAPI(client: client)
|
||||
pushAPI = PushAPI(client: client)
|
||||
scenicPermissionAPI = ScenicPermissionAPI(client: client)
|
||||
scenicSettlementAPI = ScenicSettlementAPI(client: client)
|
||||
messageCenterAPI = MessageCenterAPI(client: client)
|
||||
scenicQueueAPI = ScenicQueueAPI(client: client)
|
||||
liveAPI = LiveAPI(client: client)
|
||||
operatingAreaAPI = OperatingAreaAPI(client: client)
|
||||
pilotCertificationAPI = PilotCertificationAPI(client: client)
|
||||
taskAPI = TaskAPI(client: client)
|
||||
projectAPI = ProjectAPI(client: client)
|
||||
scheduleAPI = ScheduleAPI(client: client)
|
||||
inviteAPI = InviteAPI(client: client)
|
||||
assetsAPI = AssetsAPI(client: client)
|
||||
punchPointAPI = PunchPointAPI(client: client)
|
||||
locationReportAPI = LocationReportAPI(client: client)
|
||||
authSessionCoordinator = AuthSessionCoordinator(
|
||||
tokenStore: tokenStore,
|
||||
snapshotStore: snapshotStore,
|
||||
preferencesStore: AppPreferencesStore()
|
||||
context = AppContextBundle(
|
||||
accountContext: AccountContext(),
|
||||
permissionContext: PermissionContext(),
|
||||
scenicSpotContext: ScenicSpotContext()
|
||||
)
|
||||
sessionBootstrapper = SessionBootstrapper(
|
||||
tokenStore: tokenStore,
|
||||
snapshotStore: snapshotStore
|
||||
network = NetworkBundle(apiClient: apiClient)
|
||||
ui = UIFeedbackBundle(
|
||||
toastCenter: ToastCenter(),
|
||||
globalLoading: GlobalLoadingCenter()
|
||||
)
|
||||
apiClient.bindAuthTokenProvider { [unowned self] in
|
||||
appSession.token
|
||||
}
|
||||
runtime = AppRuntimeBundle(scenicQueueRuntime: ScenicQueueRuntime())
|
||||
session = SessionBundle(
|
||||
appSession: AppSession(),
|
||||
authSessionCoordinator: AuthSessionCoordinator(
|
||||
tokenStore: tokenStore,
|
||||
snapshotStore: snapshotStore,
|
||||
preferencesStore: preferencesStore
|
||||
),
|
||||
sessionBootstrapper: SessionBootstrapper(
|
||||
tokenStore: tokenStore,
|
||||
snapshotStore: snapshotStore
|
||||
)
|
||||
)
|
||||
bindAuthTokenProvider()
|
||||
}
|
||||
|
||||
/// 测试专用初始化,允许注入 mock `APIClient` 与存储替身。
|
||||
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(
|
||||
tokenStore: tokenStore,
|
||||
snapshotStore: snapshotStore,
|
||||
preferencesStore: preferencesStore
|
||||
),
|
||||
sessionBootstrapper: SessionBootstrapper(
|
||||
tokenStore: tokenStore,
|
||||
snapshotStore: snapshotStore
|
||||
)
|
||||
)
|
||||
bindAuthTokenProvider()
|
||||
}
|
||||
|
||||
/// 绑定推送与 UIKit 导航。
|
||||
@ -121,4 +154,11 @@ final class AppServices {
|
||||
router: appRouter
|
||||
)
|
||||
}
|
||||
|
||||
/// 将 `APIClient` 与当前 `AppSession` token 关联。
|
||||
private func bindAuthTokenProvider() {
|
||||
apiClient.bindAuthTokenProvider { [unowned self] in
|
||||
appSession.token
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
96
suixinkan_ios/App/AppServicesBundles.swift
Normal file
96
suixinkan_ios/App/AppServicesBundles.swift
Normal file
@ -0,0 +1,96 @@
|
||||
//
|
||||
// AppServicesBundles.swift
|
||||
// suixinkan
|
||||
//
|
||||
// Created by Codex on 2026/6/26.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@MainActor
|
||||
/// 登录与会话恢复相关依赖分组。
|
||||
struct SessionBundle {
|
||||
let appSession: AppSession
|
||||
let authSessionCoordinator: AuthSessionCoordinator
|
||||
let sessionBootstrapper: SessionBootstrapper
|
||||
}
|
||||
|
||||
@MainActor
|
||||
/// 账号、权限与景区景点等业务作用域 Context 分组。
|
||||
struct AppContextBundle {
|
||||
let accountContext: AccountContext
|
||||
let permissionContext: PermissionContext
|
||||
let scenicSpotContext: ScenicSpotContext
|
||||
}
|
||||
|
||||
@MainActor
|
||||
/// 网络层依赖分组,共享同一 `APIClient` 与 token 链路。
|
||||
struct NetworkBundle {
|
||||
let apiClient: APIClient
|
||||
let authAPI: AuthAPI
|
||||
let profileAPI: ProfileAPI
|
||||
let uploadAPI: UploadAPI
|
||||
let ossUploadService: OSSUploadService
|
||||
let accountContextAPI: AccountContextAPI
|
||||
let ordersAPI: OrdersAPI
|
||||
let statisticsAPI: StatisticsAPI
|
||||
let paymentAPI: PaymentAPI
|
||||
let walletAPI: WalletAPI
|
||||
let pushAPI: PushAPI
|
||||
let scenicPermissionAPI: ScenicPermissionAPI
|
||||
let scenicSettlementAPI: ScenicSettlementAPI
|
||||
let messageCenterAPI: MessageCenterAPI
|
||||
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。
|
||||
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
|
||||
/// 全局 UI 反馈(Toast、Loading)分组。
|
||||
struct UIFeedbackBundle {
|
||||
let toastCenter: ToastCenter
|
||||
let globalLoading: GlobalLoadingCenter
|
||||
}
|
||||
|
||||
@MainActor
|
||||
/// 与登录态联动的后台运行时依赖分组。
|
||||
struct AppRuntimeBundle {
|
||||
let scenicQueueRuntime: ScenicQueueRuntime
|
||||
}
|
||||
@ -9,8 +9,6 @@ import UIKit
|
||||
/// ViewController 通用工具,封装 Toast、Loading 和 ViewModel 绑定。
|
||||
@MainActor
|
||||
enum ViewControllerHelpers {
|
||||
static var services: AppServices { AppServices.shared }
|
||||
|
||||
/// 绑定 ViewModel 的 onChange 回调并在 deinit 时自动清理。
|
||||
static func bind(onChange: (() -> Void)?, owner: AnyObject, handler: @escaping () -> Void) {
|
||||
onChange?()
|
||||
|
||||
@ -42,7 +42,8 @@ final class TitleSubtitleTableViewCell: UITableViewCell {
|
||||
|
||||
/// 模块列表页基类,封装 UITableView Diffable、下拉刷新和 ViewModel onChange 绑定。
|
||||
class ModuleTableViewController: UIViewController, UITableViewDelegate {
|
||||
let services = AppServices.shared
|
||||
/// 全局依赖容器快捷访问,等价于 `appServices`。
|
||||
var services: AppServices { appServices }
|
||||
let tableView = UITableView(frame: .zero, style: .insetGrouped)
|
||||
private let refreshControl = UIRefreshControl()
|
||||
private let activityIndicator = UIActivityIndicatorView(style: .medium)
|
||||
@ -289,7 +290,8 @@ private extension Array {
|
||||
|
||||
/// 模块复杂列表基类,封装 UICollectionView Diffable 与下拉刷新。
|
||||
class ModuleCollectionViewController: UIViewController {
|
||||
let services = AppServices.shared
|
||||
/// 全局依赖容器快捷访问,等价于 `appServices`。
|
||||
var services: AppServices { appServices }
|
||||
let collectionView: UICollectionView
|
||||
private let refreshControl = UIRefreshControl()
|
||||
private let activityIndicator = UIActivityIndicatorView(style: .medium)
|
||||
|
||||
@ -10,7 +10,6 @@ import UIKit
|
||||
|
||||
/// 摄影师邀请页。
|
||||
final class PhotographerInviteViewController: UIViewController {
|
||||
private let services = AppServices.shared
|
||||
private let viewModel = PhotographerInviteViewModel()
|
||||
private let scrollView = UIScrollView()
|
||||
private let contentStack = UIStackView()
|
||||
@ -27,7 +26,7 @@ final class PhotographerInviteViewController: UIViewController {
|
||||
view.backgroundColor = UIColor(hex: 0xF5F7FA)
|
||||
setupUI()
|
||||
viewModel.onChange = { [weak self] in self?.render() }
|
||||
Task { await viewModel.reload(api: services.inviteAPI) }
|
||||
Task { await viewModel.reload(api: appServices.inviteAPI) }
|
||||
}
|
||||
|
||||
/// 初始化UI相关 UI 或状态。
|
||||
|
||||
@ -15,7 +15,6 @@ final class OperatingAreaViewController: UIViewController {
|
||||
private lazy var blockReasonView = AppContentUnavailableView(title: "无法展示地图", systemImage: "mappin.slash")
|
||||
|
||||
private var dataSource: UITableViewDiffableDataSource<Int, String>!
|
||||
private var services: AppServices { AppServices.shared }
|
||||
|
||||
/// 视图加载完成后的 UI 初始化与数据绑定。
|
||||
override func viewDidLoad() {
|
||||
@ -112,12 +111,12 @@ final class OperatingAreaViewController: UIViewController {
|
||||
private func scopeText() -> String {
|
||||
switch viewModel.mode {
|
||||
case .storeAdmin:
|
||||
return services.accountContext.currentStore?.name ?? "当前店铺"
|
||||
return appServices.accountContext.currentStore?.name ?? "当前店铺"
|
||||
case .scenicAdmin:
|
||||
return services.accountContext.currentScenic?.name ?? "当前景区"
|
||||
return appServices.accountContext.currentScenic?.name ?? "当前景区"
|
||||
case nil:
|
||||
return services.accountContext.currentScenic?.name
|
||||
?? services.accountContext.currentStore?.name
|
||||
return appServices.accountContext.currentScenic?.name
|
||||
?? appServices.accountContext.currentStore?.name
|
||||
?? "未选择业务范围"
|
||||
}
|
||||
}
|
||||
@ -130,16 +129,16 @@ final class OperatingAreaViewController: UIViewController {
|
||||
}
|
||||
|
||||
private func reload(showLoading: Bool) async {
|
||||
if showLoading { services.globalLoading.show() }
|
||||
defer { if showLoading { services.globalLoading.hide() } }
|
||||
if showLoading { appServices.globalLoading.show() }
|
||||
defer { if showLoading { appServices.globalLoading.hide() } }
|
||||
|
||||
await viewModel.reload(
|
||||
api: services.operatingAreaAPI,
|
||||
accountContext: services.accountContext,
|
||||
permissionContext: services.permissionContext
|
||||
api: appServices.operatingAreaAPI,
|
||||
accountContext: appServices.accountContext,
|
||||
permissionContext: appServices.permissionContext
|
||||
)
|
||||
if let message = viewModel.errorMessage {
|
||||
services.toastCenter.show(message)
|
||||
appServices.toastCenter.show(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,7 +10,6 @@ import UIKit
|
||||
|
||||
/// 收款页。
|
||||
final class PaymentCollectionViewController: UIViewController {
|
||||
private let services = AppServices.shared
|
||||
private let viewModel = PaymentCollectionViewModel()
|
||||
private let qrImageView = UIImageView()
|
||||
private let statusLabel = UILabel()
|
||||
@ -25,7 +24,7 @@ final class PaymentCollectionViewController: UIViewController {
|
||||
view.backgroundColor = UIColor(hex: 0xF5F7FA)
|
||||
setupUI()
|
||||
viewModel.onChange = { [weak self] in self?.render() }
|
||||
Task { await viewModel.loadPayCode(api: services.paymentAPI, scenicId: services.currentScenicId) }
|
||||
Task { await viewModel.loadPayCode(api: appServices.paymentAPI, scenicId: appServices.currentScenicId) }
|
||||
}
|
||||
|
||||
/// 初始化UI相关 UI 或状态。
|
||||
@ -76,8 +75,8 @@ final class PaymentCollectionViewController: UIViewController {
|
||||
@objc private func togglePolling() {
|
||||
Task {
|
||||
await viewModel.pollUntilPaymentDetected(
|
||||
api: services.paymentAPI,
|
||||
scenicId: services.currentScenicId
|
||||
api: appServices.paymentAPI,
|
||||
scenicId: appServices.currentScenicId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -141,8 +141,6 @@ final class PunchPointEditorViewController: UIViewController {
|
||||
private let mapPicker = PunchPointMapPickerView()
|
||||
private let locationProvider = ForegroundLocationProvider()
|
||||
|
||||
private var services: AppServices { AppServices.shared }
|
||||
|
||||
init(punchPointId: Int?) {
|
||||
self.punchPointId = punchPointId
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
@ -175,7 +173,7 @@ final class PunchPointEditorViewController: UIViewController {
|
||||
|
||||
Task {
|
||||
guard let punchPointId else { return }
|
||||
if let detail = try? await services.punchPointAPI.punchPointInfo(id: punchPointId) {
|
||||
if let detail = try? await appServices.punchPointAPI.punchPointInfo(id: punchPointId) {
|
||||
viewModel.apply(detail)
|
||||
syncFieldsFromViewModel()
|
||||
}
|
||||
@ -252,7 +250,7 @@ final class PunchPointEditorViewController: UIViewController {
|
||||
)
|
||||
syncFieldsFromViewModel()
|
||||
} catch {
|
||||
services.toastCenter.show("定位失败:\(error.localizedDescription)")
|
||||
appServices.toastCenter.show("定位失败:\(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -264,15 +262,15 @@ final class PunchPointEditorViewController: UIViewController {
|
||||
viewModel.longitudeText = longitudeField.text ?? ""
|
||||
Task {
|
||||
let success = await viewModel.submit(
|
||||
scenicId: services.currentScenicId,
|
||||
api: services.punchPointAPI,
|
||||
uploadService: services.ossUploadService
|
||||
scenicId: appServices.currentScenicId,
|
||||
api: appServices.punchPointAPI,
|
||||
uploadService: appServices.ossUploadService
|
||||
)
|
||||
if success {
|
||||
services.toastCenter.show("保存成功")
|
||||
appServices.toastCenter.show("保存成功")
|
||||
navigationController?.popViewController(animated: true)
|
||||
} else if let message = viewModel.errorMessage {
|
||||
services.toastCenter.show(message)
|
||||
appServices.toastCenter.show(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user