Introduce GlobalLoadingCenter with reference counting, wire it through RootView and major auth/order/profile screens, and add the loading animation asset plus unit tests. Co-authored-by: Cursor <cursoragent@cursor.com>
155 lines
5.9 KiB
Swift
155 lines
5.9 KiB
Swift
//
|
||
// RootView.swift
|
||
// suixinkan
|
||
//
|
||
// Created by Codex on 2026/6/18.
|
||
//
|
||
|
||
import SwiftUI
|
||
|
||
/// App 根视图,负责创建全局依赖并根据登录状态切换登录页和主界面。
|
||
struct RootView: View {
|
||
@State private var appSession = AppSession()
|
||
@State private var accountContext = AccountContext()
|
||
@State private var permissionContext = PermissionContext()
|
||
@State private var scenicSpotContext = ScenicSpotContext()
|
||
@State private var appRouter = AppRouter()
|
||
@State private var toastCenter = ToastCenter()
|
||
@State private var globalLoading = GlobalLoadingCenter()
|
||
@State private var snapshotStore: AccountSnapshotStore
|
||
@State private var apiClient: APIClient
|
||
@State private var authAPI: AuthAPI
|
||
@State private var profileAPI: ProfileAPI
|
||
@State private var uploadAPI: UploadAPI
|
||
@State private var ossUploadService: OSSUploadService
|
||
@State private var accountContextAPI: AccountContextAPI
|
||
@State private var ordersAPI: OrdersAPI
|
||
@State private var statisticsAPI: StatisticsAPI
|
||
@State private var paymentAPI: PaymentAPI
|
||
@State private var walletAPI: WalletAPI
|
||
@State private var scenicPermissionAPI: ScenicPermissionAPI
|
||
@State private var authSessionCoordinator: AuthSessionCoordinator
|
||
@State private var sessionBootstrapper: SessionBootstrapper
|
||
|
||
/// 初始化网络客户端和业务 API,确保它们共享同一个 token provider。
|
||
init() {
|
||
let apiClient = APIClient()
|
||
let tokenStore = SessionTokenStore()
|
||
let snapshotStore = AccountSnapshotStore()
|
||
_snapshotStore = State(initialValue: snapshotStore)
|
||
_apiClient = State(initialValue: apiClient)
|
||
_authAPI = State(initialValue: AuthAPI(client: apiClient))
|
||
_profileAPI = State(initialValue: ProfileAPI(client: apiClient))
|
||
let uploadAPI = UploadAPI(client: apiClient)
|
||
_uploadAPI = State(initialValue: uploadAPI)
|
||
_ossUploadService = State(initialValue: OSSUploadService(configService: uploadAPI))
|
||
_accountContextAPI = State(initialValue: AccountContextAPI(client: apiClient))
|
||
_ordersAPI = State(initialValue: OrdersAPI(client: apiClient))
|
||
_statisticsAPI = State(initialValue: StatisticsAPI(client: apiClient))
|
||
_paymentAPI = State(initialValue: PaymentAPI(client: apiClient))
|
||
_walletAPI = State(initialValue: WalletAPI(client: apiClient))
|
||
_scenicPermissionAPI = State(initialValue: ScenicPermissionAPI(client: apiClient))
|
||
_authSessionCoordinator = State(
|
||
initialValue: AuthSessionCoordinator(
|
||
tokenStore: tokenStore,
|
||
snapshotStore: snapshotStore,
|
||
preferencesStore: AppPreferencesStore()
|
||
)
|
||
)
|
||
_sessionBootstrapper = State(
|
||
initialValue: SessionBootstrapper(
|
||
tokenStore: tokenStore,
|
||
snapshotStore: snapshotStore
|
||
)
|
||
)
|
||
}
|
||
|
||
var body: some View {
|
||
rootContent
|
||
.globalToastOverlay()
|
||
.globalLoadingOverlay(loadingCenter: globalLoading)
|
||
.environment(appSession)
|
||
.environment(accountContext)
|
||
.environment(permissionContext)
|
||
.environment(scenicSpotContext)
|
||
.environment(appRouter)
|
||
.environment(toastCenter)
|
||
.environment(\.globalLoading, globalLoading)
|
||
.environment(snapshotStore)
|
||
.environment(apiClient)
|
||
.environment(authAPI)
|
||
.environment(profileAPI)
|
||
.environment(uploadAPI)
|
||
.environment(ossUploadService)
|
||
.environment(accountContextAPI)
|
||
.environment(ordersAPI)
|
||
.environment(statisticsAPI)
|
||
.environment(paymentAPI)
|
||
.environment(walletAPI)
|
||
.environment(scenicPermissionAPI)
|
||
.environment(authSessionCoordinator)
|
||
.task {
|
||
apiClient.bindAuthTokenProvider { appSession.token }
|
||
await globalLoading.withLoading {
|
||
await sessionBootstrapper.restore(
|
||
appSession: appSession,
|
||
accountContext: accountContext,
|
||
permissionContext: permissionContext,
|
||
profileAPI: profileAPI,
|
||
accountContextAPI: accountContextAPI,
|
||
toastCenter: toastCenter
|
||
)
|
||
}
|
||
}
|
||
.task(id: scenicSpotTaskID) {
|
||
guard appSession.isLoggedIn else {
|
||
scenicSpotContext.reset()
|
||
return
|
||
}
|
||
await scenicSpotContext.reload(
|
||
scenicId: accountContext.currentScenic?.id,
|
||
api: accountContextAPI
|
||
)
|
||
}
|
||
.onChange(of: appSession.phase) { _, phase in
|
||
guard phase == .loggedOut else { return }
|
||
accountContext.reset()
|
||
permissionContext.reset()
|
||
scenicSpotContext.reset()
|
||
appRouter.reset()
|
||
toastCenter.dismiss()
|
||
}
|
||
}
|
||
|
||
@ViewBuilder
|
||
private var rootContent: some View {
|
||
switch appSession.phase {
|
||
case .loggedOut:
|
||
LoginView()
|
||
case .restoring:
|
||
Color(.systemBackground)
|
||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||
case .loggedIn:
|
||
MainTabsView()
|
||
}
|
||
}
|
||
|
||
/// 景点加载任务的稳定标识,确保登录状态和当前景区变化都会触发任务。
|
||
private var scenicSpotTaskID: String {
|
||
let phaseText: String
|
||
switch appSession.phase {
|
||
case .loggedOut:
|
||
phaseText = "loggedOut"
|
||
case .restoring:
|
||
phaseText = "restoring"
|
||
case .loggedIn:
|
||
phaseText = "loggedIn"
|
||
}
|
||
return "\(phaseText)-\(accountContext.currentScenic?.id ?? 0)"
|
||
}
|
||
}
|
||
|
||
#Preview {
|
||
RootView()
|
||
}
|