// // SportsTimeApp.swift // SportsTime // // Created by Trey Tartt on 1/6/26. // import SwiftUI import SwiftData @main struct SportsTimeApp: App { var sharedModelContainer: ModelContainer = { let schema = Schema([ // User data models SavedTrip.self, TripVote.self, UserPreferences.self, CachedSchedule.self, // Stadium progress models StadiumVisit.self, VisitPhotoMetadata.self, Achievement.self, CachedGameScore.self, // Canonical data models SyncState.self, CanonicalStadium.self, StadiumAlias.self, CanonicalTeam.self, TeamAlias.self, LeagueStructureModel.self, CanonicalGame.self, ]) let modelConfiguration = ModelConfiguration( schema: schema, isStoredInMemoryOnly: false, cloudKitDatabase: .none // Local only; CloudKit used separately for schedules ) do { return try ModelContainer(for: schema, configurations: [modelConfiguration]) } catch { fatalError("Could not create ModelContainer: \(error)") } }() var body: some Scene { WindowGroup { BootstrappedContentView(modelContainer: sharedModelContainer) } .modelContainer(sharedModelContainer) } } // MARK: - Bootstrapped Content View /// Wraps the main content with bootstrap logic. /// Shows a loading indicator until bootstrap completes, then shows HomeView. struct BootstrappedContentView: View { let modelContainer: ModelContainer @State private var isBootstrapping = true @State private var bootstrapError: Error? var body: some View { Group { if isBootstrapping { BootstrapLoadingView() } else if let error = bootstrapError { BootstrapErrorView(error: error) { Task { await performBootstrap() } } } else { HomeView() } } .task { await performBootstrap() } } @MainActor private func performBootstrap() async { isBootstrapping = true bootstrapError = nil let context = modelContainer.mainContext let bootstrapService = BootstrapService() do { // 1. Bootstrap from bundled JSON if first launch (no data exists) try await bootstrapService.bootstrapIfNeeded(context: context) // 2. Configure DataProvider with SwiftData context AppDataProvider.shared.configure(with: context) // 3. Load data from SwiftData into memory await AppDataProvider.shared.loadInitialData() // 4. App is now usable isBootstrapping = false // 5. Background: Try to refresh from CloudKit (non-blocking) Task.detached(priority: .background) { await self.performBackgroundSync(context: context) } } catch { bootstrapError = error isBootstrapping = false } } @MainActor private func performBackgroundSync(context: ModelContext) async { let syncService = CanonicalSyncService() do { let result = try await syncService.syncAll(context: context) // If any data was updated, reload the DataProvider if !result.isEmpty { await AppDataProvider.shared.loadInitialData() print("CloudKit sync completed: \(result.totalUpdated) items updated") } } catch CanonicalSyncService.SyncError.cloudKitUnavailable { // Offline or CloudKit not available - silently continue with local data print("CloudKit unavailable, using local data") } catch { // Other sync errors - log but don't interrupt user print("Background sync error: \(error.localizedDescription)") } } } // MARK: - Bootstrap Loading View struct BootstrapLoadingView: View { var body: some View { VStack(spacing: 20) { ThemedSpinner(size: 50, lineWidth: 4) Text("Setting up SportsTime...") .font(.headline) .foregroundStyle(.secondary) } } } // MARK: - Bootstrap Error View struct BootstrapErrorView: View { let error: Error let onRetry: () -> Void var body: some View { VStack(spacing: 20) { Image(systemName: "exclamationmark.triangle.fill") .font(.system(size: 50)) .foregroundStyle(.orange) Text("Setup Failed") .font(.title2) .fontWeight(.semibold) Text(error.localizedDescription) .font(.subheadline) .foregroundStyle(.secondary) .multilineTextAlignment(.center) .padding(.horizontal) Button("Try Again") { onRetry() } .buttonStyle(.borderedProminent) } .padding() } }