Files
Sportstime/SportsTime/SportsTimeApp.swift
Trey t c0f1645434 feat(ui): replace loading indicators with Apple-style LoadingSpinner
- Add LoadingSpinner component with small/medium/large sizes using system gray color
- Add LoadingPlaceholder for skeleton loading states
- Add LoadingSheet for full-screen blocking overlays
- Replace ThemedSpinner/ThemedSpinnerCompact across all views
- Remove deprecated loading components from AnimatedComponents.swift
- Delete LoadingTextGenerator.swift
- Fix PhotoImportView layout to fill full width

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 22:43:33 -06:00

194 lines
5.7 KiB
Swift

//
// 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
@Environment(\.scenePhase) private var scenePhase
@State private var isBootstrapping = true
@State private var bootstrapError: Error?
@State private var hasCompletedInitialSync = false
var body: some View {
Group {
if isBootstrapping {
BootstrapLoadingView()
} else if let error = bootstrapError {
BootstrapErrorView(error: error) {
Task {
await performBootstrap()
}
}
} else {
HomeView()
}
}
.task {
await performBootstrap()
}
.onChange(of: scenePhase) { _, newPhase in
// Sync when app comes to foreground (but not on initial launch)
if newPhase == .active && hasCompletedInitialSync {
Task {
await performBackgroundSync(context: modelContainer.mainContext)
}
}
}
}
@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)
await MainActor.run {
self.hasCompletedInitialSync = true
}
}
} 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) {
LoadingSpinner(size: .large)
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()
}
}