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 <noreply@anthropic.com>
This commit is contained in:
@@ -18,7 +18,12 @@ struct GamesHistoryView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle("Games Attended")
|
.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) {
|
if let stadium = AppDataProvider.shared.stadium(for: visit.stadiumId) {
|
||||||
VisitDetailView(visit: visit, stadium: stadium)
|
VisitDetailView(visit: visit, stadium: stadium)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,12 +26,6 @@ final class SettingsViewModel {
|
|||||||
didSet { savePreferences() }
|
didSet { savePreferences() }
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Sync State
|
|
||||||
|
|
||||||
private(set) var isSyncing = false
|
|
||||||
private(set) var lastSyncDate: Date?
|
|
||||||
private(set) var syncError: String?
|
|
||||||
|
|
||||||
// MARK: - App Info
|
// MARK: - App Info
|
||||||
|
|
||||||
let appVersion: String
|
let appVersion: String
|
||||||
@@ -57,9 +51,6 @@ final class SettingsViewModel {
|
|||||||
let savedDrivingHours = defaults.integer(forKey: "maxDrivingHoursPerDay")
|
let savedDrivingHours = defaults.integer(forKey: "maxDrivingHoursPerDay")
|
||||||
self.maxDrivingHoursPerDay = savedDrivingHours == 0 ? 8 : savedDrivingHours
|
self.maxDrivingHoursPerDay = savedDrivingHours == 0 ? 8 : savedDrivingHours
|
||||||
|
|
||||||
// Last sync
|
|
||||||
self.lastSyncDate = defaults.object(forKey: "lastSyncDate") as? Date
|
|
||||||
|
|
||||||
// App info
|
// App info
|
||||||
self.appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0"
|
self.appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0"
|
||||||
self.buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "1"
|
self.buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "1"
|
||||||
@@ -67,19 +58,6 @@ final class SettingsViewModel {
|
|||||||
|
|
||||||
// MARK: - Actions
|
// 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) {
|
func toggleSport(_ sport: Sport) {
|
||||||
if selectedSports.contains(sport) {
|
if selectedSports.contains(sport) {
|
||||||
// Don't allow removing all sports
|
// Don't allow removing all sports
|
||||||
|
|||||||
@@ -21,9 +21,6 @@ struct SettingsView: View {
|
|||||||
// Travel Preferences
|
// Travel Preferences
|
||||||
travelSection
|
travelSection
|
||||||
|
|
||||||
// Data Sync
|
|
||||||
dataSection
|
|
||||||
|
|
||||||
// About
|
// About
|
||||||
aboutSection
|
aboutSection
|
||||||
|
|
||||||
@@ -149,57 +146,6 @@ struct SettingsView: View {
|
|||||||
.listRowBackground(Theme.cardBackground(colorScheme))
|
.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
|
// MARK: - About Section
|
||||||
|
|
||||||
private var aboutSection: some View {
|
private var aboutSection: some View {
|
||||||
|
|||||||
@@ -59,8 +59,10 @@ struct SportsTimeApp: App {
|
|||||||
struct BootstrappedContentView: View {
|
struct BootstrappedContentView: View {
|
||||||
let modelContainer: ModelContainer
|
let modelContainer: ModelContainer
|
||||||
|
|
||||||
|
@Environment(\.scenePhase) private var scenePhase
|
||||||
@State private var isBootstrapping = true
|
@State private var isBootstrapping = true
|
||||||
@State private var bootstrapError: Error?
|
@State private var bootstrapError: Error?
|
||||||
|
@State private var hasCompletedInitialSync = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
@@ -79,6 +81,14 @@ struct BootstrappedContentView: View {
|
|||||||
.task {
|
.task {
|
||||||
await performBootstrap()
|
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
|
@MainActor
|
||||||
@@ -105,6 +115,9 @@ struct BootstrappedContentView: View {
|
|||||||
// 5. Background: Try to refresh from CloudKit (non-blocking)
|
// 5. Background: Try to refresh from CloudKit (non-blocking)
|
||||||
Task.detached(priority: .background) {
|
Task.detached(priority: .background) {
|
||||||
await self.performBackgroundSync(context: context)
|
await self.performBackgroundSync(context: context)
|
||||||
|
await MainActor.run {
|
||||||
|
self.hasCompletedInitialSync = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
bootstrapError = error
|
bootstrapError = error
|
||||||
|
|||||||
Reference in New Issue
Block a user