// // RootView.swift // suixinkan // // Created by Codex on 2026/6/18. // import SwiftUI /// 根视图内容展示状态,隔离冷启动判定与登录阶段切换。 enum RootContentDisplayState: Equatable { case bootstrapPlaceholder case login case restoringPlaceholder case mainTabs /// 根据冷启动 bootstrap 状态和登录阶段计算当前应创建的根内容。 static func resolve(isColdStartBootstrapPending: Bool, appPhase: AuthPhase) -> RootContentDisplayState { if isColdStartBootstrapPending { return .bootstrapPlaceholder } switch appPhase { case .loggedOut: return .login case .restoring: return .restoringPlaceholder case .loggedIn: return .mainTabs } } } /// App 根视图,负责创建全局依赖并根据登录状态切换登录页和主界面。 struct RootView: View { @Environment(\.scenePhase) private var scenePhase @StateObject private var appSession = AppSession() @StateObject private var accountContext = AccountContext() @StateObject private var permissionContext = PermissionContext() @StateObject private var scenicSpotContext = ScenicSpotContext() @StateObject private var appRouter = AppRouter() @StateObject 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 pushAPI: PushAPI @State private var scenicPermissionAPI: ScenicPermissionAPI @State private var scenicSettlementAPI: ScenicSettlementAPI @State private var messageCenterAPI: MessageCenterAPI @State private var scenicQueueAPI: ScenicQueueAPI @StateObject private var scenicQueueRuntime = ScenicQueueRuntime() @State private var liveAPI: LiveAPI @State private var operatingAreaAPI: OperatingAreaAPI @State private var pilotCertificationAPI: PilotCertificationAPI @State private var taskAPI: TaskAPI @State private var projectAPI: ProjectAPI @State private var scheduleAPI: ScheduleAPI @State private var inviteAPI: InviteAPI @State private var assetsAPI: AssetsAPI @State private var travelAlbumAPI: TravelAlbumAPI @State private var punchPointAPI: PunchPointAPI @State private var locationReportAPI: LocationReportAPI @StateObject private var homeLocationViewModel: HomeLocationViewModel @State private var cooperationOrderAPI: CooperationOrderAPI @State private var authSessionCoordinator: AuthSessionCoordinator @State private var sessionBootstrapper: SessionBootstrapper @State private var isColdStartBootstrapPending = true @State private var appConfigAPI: AppConfigAPI @State private var preferencesStore: AppPreferencesStore /// 初始化网络客户端和业务 API,确保它们共享同一个 token provider。 init() { let apiClient = APIClient() let tokenStore = SessionTokenStore() let snapshotStore = AccountSnapshotStore() let preferencesStore = AppPreferencesStore() _snapshotStore = State(initialValue: snapshotStore) _apiClient = State(initialValue: apiClient) _authAPI = State(initialValue: AuthAPI(client: apiClient)) _appConfigAPI = State(initialValue: AppConfigAPI(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)) _pushAPI = State(initialValue: PushAPI(client: apiClient)) _scenicPermissionAPI = State(initialValue: ScenicPermissionAPI(client: apiClient)) _scenicSettlementAPI = State(initialValue: ScenicSettlementAPI(client: apiClient)) _messageCenterAPI = State(initialValue: MessageCenterAPI(client: apiClient)) _scenicQueueAPI = State(initialValue: ScenicQueueAPI(client: apiClient)) _liveAPI = State(initialValue: LiveAPI(client: apiClient)) _operatingAreaAPI = State(initialValue: OperatingAreaAPI(client: apiClient)) _pilotCertificationAPI = State(initialValue: PilotCertificationAPI(client: apiClient)) _taskAPI = State(initialValue: TaskAPI(client: apiClient)) _projectAPI = State(initialValue: ProjectAPI(client: apiClient)) _scheduleAPI = State(initialValue: ScheduleAPI(client: apiClient)) _inviteAPI = State(initialValue: InviteAPI(client: apiClient)) _assetsAPI = State(initialValue: AssetsAPI(client: apiClient)) _travelAlbumAPI = State(initialValue: TravelAlbumAPI(client: apiClient)) _punchPointAPI = State(initialValue: PunchPointAPI(client: apiClient)) let locationReportAPI = LocationReportAPI(client: apiClient) _locationReportAPI = State(initialValue: locationReportAPI) _homeLocationViewModel = StateObject(wrappedValue: HomeLocationViewModel(api: locationReportAPI)) _cooperationOrderAPI = State(initialValue: CooperationOrderAPI(client: apiClient)) _preferencesStore = State(initialValue: preferencesStore) _authSessionCoordinator = State( initialValue: AuthSessionCoordinator( tokenStore: tokenStore, snapshotStore: snapshotStore, preferencesStore: preferencesStore, onPrivacyAgreementPersisted: { accepted in guard accepted else { return } AMapBootstrap.configure(apiKey: AMapConfig.apiKey) UMengSDKBootstrap.shared.configureIfAllowed(preferencesStore: preferencesStore) } ) ) _sessionBootstrapper = State( initialValue: SessionBootstrapper( tokenStore: tokenStore, snapshotStore: snapshotStore ) ) } var body: some View { ZStack { rootContent if isColdStartBootstrapPending { SplashView() .transition(.opacity) .zIndex(1) } } .globalToastOverlay() .globalLoadingOverlay(loadingCenter: globalLoading) .environmentObject(appSession) .environmentObject(accountContext) .environmentObject(permissionContext) .environmentObject(scenicSpotContext) .environmentObject(appRouter) .environmentObject(toastCenter) .environment(\.globalLoading, globalLoading) .environment(\.accountSnapshotStore, snapshotStore) .environment(\.apiClient, apiClient) .environment(\.authAPI, authAPI) .environment(\.appConfigAPI, appConfigAPI) .environment(\.profileAPI, profileAPI) .environment(\.uploadAPI, uploadAPI) .environment(\.ossUploadService, ossUploadService) .environment(\.accountContextAPI, accountContextAPI) .environment(\.ordersAPI, ordersAPI) .environment(\.statisticsAPI, statisticsAPI) .environment(\.paymentAPI, paymentAPI) .environment(\.walletAPI, walletAPI) .environment(\.pushAPI, pushAPI) .environment(\.scenicPermissionAPI, scenicPermissionAPI) .environment(\.scenicSettlementAPI, scenicSettlementAPI) .environment(\.messageCenterAPI, messageCenterAPI) .environment(\.scenicQueueAPI, scenicQueueAPI) .environmentObject(scenicQueueRuntime) .environment(\.liveAPI, liveAPI) .environment(\.operatingAreaAPI, operatingAreaAPI) .environment(\.pilotCertificationAPI, pilotCertificationAPI) .environment(\.taskAPI, taskAPI) .environment(\.projectAPI, projectAPI) .environment(\.scheduleAPI, scheduleAPI) .environment(\.inviteAPI, inviteAPI) .environment(\.assetsAPI, assetsAPI) .environment(\.travelAlbumAPI, travelAlbumAPI) .environment(\.punchPointAPI, punchPointAPI) .environment(\.locationReportAPI, locationReportAPI) .environmentObject(homeLocationViewModel) .environment(\.cooperationOrderAPI, cooperationOrderAPI) .environment(\.authSessionCoordinator, authSessionCoordinator) .task { apiClient.bindAuthTokenProvider { appSession.token } configurePrivacyGatedThirdPartySDKsIfAllowed() PushNotificationManager.shared.configure(api: pushAPI, session: appSession, router: appRouter) await sessionBootstrapper.restore( appSession: appSession, accountContext: accountContext, permissionContext: permissionContext, profileAPI: profileAPI, accountContextAPI: accountContextAPI, toastCenter: toastCenter ) withAnimation { isColdStartBootstrapPending = false } if appSession.isLoggedIn, !AppUITestLaunchState.isRunningUITests { PushNotificationManager.shared.requestAuthorizationAndRegister() } } .task(id: scenicSpotTaskID) { guard appSession.isLoggedIn else { scenicSpotContext.reset() return } await scenicSpotContext.reload( scenicId: accountContext.currentScenic?.id, api: accountContextAPI ) } .onChange(of: scenePhase) { newPhase in guard !AppUITestLaunchState.isRunningUITests else { return } if appSession.isLoggedIn { scenicQueueRuntime.update( api: scenicQueueAPI, userId: accountContext.profile?.userId, scenicId: accountContext.currentScenic?.id, scenePhase: newPhase ) } else { scenicQueueRuntime.stop() } } .onChange(of: accountContext.currentScenic?.id) { _ in guard !AppUITestLaunchState.isRunningUITests else { return } if appSession.isLoggedIn { scenicQueueRuntime.update( api: scenicQueueAPI, userId: accountContext.profile?.userId, scenicId: accountContext.currentScenic?.id, scenePhase: scenePhase ) } } .onChange(of: appSession.phase) { phase in switch phase { case .loggedIn: if !AppUITestLaunchState.isRunningUITests { PushNotificationManager.shared.requestAuthorizationAndRegister() } case .loggedOut: accountContext.reset() permissionContext.reset() scenicSpotContext.reset() appRouter.reset() toastCenter.dismiss() case .restoring: break } } } @ViewBuilder private var rootContent: some View { switch rootContentDisplayState { case .bootstrapPlaceholder, .restoringPlaceholder: Color.clear .frame(maxWidth: .infinity, maxHeight: .infinity) case .login: LoginView() case .mainTabs: MainTabsView() .task { #if DEBUG await AppUITestRouteDriver.applyIfNeeded(appRouter: appRouter) #endif } } } /// 计算根视图当前应创建的内容,避免冷启动期间提前创建登录页。 private var rootContentDisplayState: RootContentDisplayState { RootContentDisplayState.resolve( isColdStartBootstrapPending: isColdStartBootstrapPending, appPhase: appSession.phase ) } /// 景点加载任务的稳定标识,确保登录状态和当前景区变化都会触发任务。 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)" } /// 在用户已经同意隐私政策后初始化第三方 SDK。 private func configurePrivacyGatedThirdPartySDKsIfAllowed() { guard preferencesStore.loadPrivacyAgreementAccepted() else { return } AMapBootstrap.configure(apiKey: AMapConfig.apiKey) UMengSDKBootstrap.shared.configureIfAllowed(preferencesStore: preferencesStore) } } #Preview { RootView() }