Files
suixinkan_ios_new/suixinkan/App/RootView.swift
汉秋 49e997ddba 修复合作获客扫码绑定崩溃与 Toast 层级,并接入分成比例修改。
将扫码页提升到列表层弹出并补齐 AppDelegate.window,避免扫码时 UIKit 取主窗口崩溃;Toast 改用独立 UIWindow 显示在 sheet 之上;合作获客员支持短信验证修改分成比例,同时优化冷启动根视图创建时机并补充相关测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-03 20:37:24 +08:00

313 lines
14 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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