From ba3ea6daeb649a2f20a979583868a40f5f95db8b Mon Sep 17 00:00:00 2001 From: Trey t Date: Mon, 12 Jan 2026 22:04:10 -0600 Subject: [PATCH] fix(sync): add foreground sync, remove manual sync button - Add scenePhase observer to sync when app returns to foreground - Remove misleading "Sync Schedules" button from Settings (it only reloaded local data, didn't actually sync from CloudKit) - Fix GamesHistoryView to refresh list after deleting a visit Co-Authored-By: Claude Opus 4.5 --- .../Progress/Views/GamesHistoryView.swift | 7 ++- .../ViewModels/SettingsViewModel.swift | 22 -------- .../Settings/Views/SettingsView.swift | 54 ------------------- SportsTime/SportsTimeApp.swift | 13 +++++ 4 files changed, 19 insertions(+), 77 deletions(-) diff --git a/SportsTime/Features/Progress/Views/GamesHistoryView.swift b/SportsTime/Features/Progress/Views/GamesHistoryView.swift index 3a569ff..91f5fca 100644 --- a/SportsTime/Features/Progress/Views/GamesHistoryView.swift +++ b/SportsTime/Features/Progress/Views/GamesHistoryView.swift @@ -18,7 +18,12 @@ struct GamesHistoryView: View { } } .navigationTitle("Games Attended") - .sheet(item: $selectedVisit) { visit in + .sheet(item: $selectedVisit, onDismiss: { + // Refresh data after sheet closes (handles deletion case) + Task { + await viewModel?.loadGames() + } + }) { visit in if let stadium = AppDataProvider.shared.stadium(for: visit.stadiumId) { VisitDetailView(visit: visit, stadium: stadium) } diff --git a/SportsTime/Features/Settings/ViewModels/SettingsViewModel.swift b/SportsTime/Features/Settings/ViewModels/SettingsViewModel.swift index 9a23e28..c34cd5e 100644 --- a/SportsTime/Features/Settings/ViewModels/SettingsViewModel.swift +++ b/SportsTime/Features/Settings/ViewModels/SettingsViewModel.swift @@ -26,12 +26,6 @@ final class SettingsViewModel { didSet { savePreferences() } } - // MARK: - Sync State - - private(set) var isSyncing = false - private(set) var lastSyncDate: Date? - private(set) var syncError: String? - // MARK: - App Info let appVersion: String @@ -57,9 +51,6 @@ final class SettingsViewModel { let savedDrivingHours = defaults.integer(forKey: "maxDrivingHoursPerDay") self.maxDrivingHoursPerDay = savedDrivingHours == 0 ? 8 : savedDrivingHours - // Last sync - self.lastSyncDate = defaults.object(forKey: "lastSyncDate") as? Date - // App info self.appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0" self.buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "1" @@ -67,19 +58,6 @@ final class SettingsViewModel { // MARK: - Actions - func syncSchedules() async { - isSyncing = true - syncError = nil - - // Trigger data reload from provider - await AppDataProvider.shared.loadInitialData() - - lastSyncDate = Date() - UserDefaults.standard.set(lastSyncDate, forKey: "lastSyncDate") - - isSyncing = false - } - func toggleSport(_ sport: Sport) { if selectedSports.contains(sport) { // Don't allow removing all sports diff --git a/SportsTime/Features/Settings/Views/SettingsView.swift b/SportsTime/Features/Settings/Views/SettingsView.swift index 5997170..c2360d5 100644 --- a/SportsTime/Features/Settings/Views/SettingsView.swift +++ b/SportsTime/Features/Settings/Views/SettingsView.swift @@ -21,9 +21,6 @@ struct SettingsView: View { // Travel Preferences travelSection - // Data Sync - dataSection - // About aboutSection @@ -149,57 +146,6 @@ struct SettingsView: View { .listRowBackground(Theme.cardBackground(colorScheme)) } - // MARK: - Data Section - - private var dataSection: some View { - Section { - Button { - Task { - await viewModel.syncSchedules() - } - } label: { - HStack { - Label("Sync Schedules", systemImage: "arrow.triangle.2.circlepath") - - Spacer() - - if viewModel.isSyncing { - ThemedSpinnerCompact(size: 18) - } - } - } - .disabled(viewModel.isSyncing) - - if let lastSync = viewModel.lastSyncDate { - HStack { - Text("Last Sync") - Spacer() - Text(lastSync, style: .relative) - .foregroundStyle(.secondary) - } - } - - if let error = viewModel.syncError { - HStack { - Image(systemName: "exclamationmark.triangle") - .foregroundStyle(.red) - Text(error) - .font(.caption) - .foregroundStyle(.red) - } - } - } header: { - Text("Data") - } footer: { - #if targetEnvironment(simulator) - Text("Using stub data (Simulator mode)") - #else - Text("Schedule data is synced from CloudKit.") - #endif - } - .listRowBackground(Theme.cardBackground(colorScheme)) - } - // MARK: - About Section private var aboutSection: some View { diff --git a/SportsTime/SportsTimeApp.swift b/SportsTime/SportsTimeApp.swift index 19282a1..defbf55 100644 --- a/SportsTime/SportsTimeApp.swift +++ b/SportsTime/SportsTimeApp.swift @@ -59,8 +59,10 @@ struct SportsTimeApp: App { 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 { @@ -79,6 +81,14 @@ struct BootstrappedContentView: View { .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 @@ -105,6 +115,9 @@ struct BootstrappedContentView: View { // 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