From 8230daa2494f6410a0dbf11070b2bdad9f32c843 Mon Sep 17 00:00:00 2001 From: FuelBoard Contributor Date: Tue, 11 Aug 2026 17:43:11 +0100 Subject: [PATCH] Cache-first refresh policy: auto-fetch at most twice a day (12h gate), no fetch on launch/foreground/location/fuel-switch, pull-to-refresh forces; geofences still follow location --- FuelBoard/ContentView.swift | 24 +++++++++++++++++------- FuelBoard/StationsView.swift | 5 +++++ Shared/FuelStore.swift | 24 ++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 7 deletions(-) diff --git a/FuelBoard/ContentView.swift b/FuelBoard/ContentView.swift index 641acde..4de059b 100644 --- a/FuelBoard/ContentView.swift +++ b/FuelBoard/ContentView.swift @@ -87,7 +87,8 @@ struct ContentView: View { location: location, radiusKM: alertsRadius, favouriteIDs: favouriteIDs, - onToggleFavourite: toggleFavourite + onToggleFavourite: toggleFavourite, + onRefresh: { await refresh(force: true) } ) .tabItem { Label("Stations", systemImage: "fuelpump.fill") } @@ -113,6 +114,7 @@ struct ContentView: View { monitor.update(stations: stations, favourites: refreshedFavourites, fuel: selectedFuel, radiusKM: alertsRadius) monitor.setEnabled(alertsEnabled) + // Refresh only when the cache is stale (twice-a-day policy). Task { await refresh() } } .onChange(of: scenePhase) { _, newPhase in @@ -120,7 +122,7 @@ struct ContentView: View { locationManager.startForegroundTracking() monitor.update(stations: stations, favourites: refreshedFavourites, fuel: selectedFuel, radiusKM: alertsRadius) - Task { await refresh() } + // No network fetch on foreground — pull-to-refresh is the override. } else { locationManager.stopForegroundTracking() } @@ -130,13 +132,15 @@ struct ContentView: View { location = newLocation FuelStore.saveLocation(lat: newLocation.lat, lng: newLocation.lng) WidgetCenter.shared.reloadAllTimelines() - Task { await refresh() } + // Geofences follow the user's position, but the station list is + // NOT re-fetched on every movement (cached, twice-a-day policy). + monitor.update(stations: stations, favourites: refreshedFavourites, + fuel: selectedFuel, radiusKM: alertsRadius) } } .onChange(of: selectedFuel) { _, _ in - // Re-fetch so the station set matches the selected fuel (relay - // filters by grade); all prices still come back in one response. - Task { await refresh() } + // No re-fetch needed — one response carries E5/E10/DIESEL prices. + WidgetCenter.shared.reloadAllTimelines() } .onChange(of: alertsEnabled) { _, newValue in FuelStore.saveAlertsEnabled(newValue) @@ -166,13 +170,19 @@ struct ContentView: View { fuel: selectedFuel, radiusKM: alertsRadius) } - private func refresh() async { + /// Fetches fresh prices, but only when the cache is stale — unless + /// `force` is true (pull-to-refresh is the manual override). + private func refresh(force: Bool = false) async { + if !force, FuelStore.isCacheFresh { + return // data already fresh — skip network entirely + } isLoading = true defer { isLoading = false } do { let fetched = try await FuelPriceProvider.active.fetchStations(near: location?.lat, lng: location?.lng, fuel: selectedFuel) stations = fetched FuelStore.saveStations(fetched) + FuelStore.saveLastRefresh() WidgetCenter.shared.reloadAllTimelines() statusMessage = "Loaded \(fetched.count) stations · \(Date().formatted(date: .omitted, time: .shortened))" } catch { diff --git a/FuelBoard/StationsView.swift b/FuelBoard/StationsView.swift index 9f839c2..17dd28e 100644 --- a/FuelBoard/StationsView.swift +++ b/FuelBoard/StationsView.swift @@ -14,6 +14,7 @@ struct StationsView: View { let radiusKM: Double let favouriteIDs: Set var onToggleFavourite: (FuelStation) -> Void = { _ in } + var onRefresh: () async -> Void = {} var body: some View { NavigationStack { @@ -130,6 +131,10 @@ struct StationsView: View { } } } + .refreshable { + // Manual override for the twice-a-day cache policy. + await onRefresh() + } .navigationTitle("FuelBoard") } } diff --git a/Shared/FuelStore.swift b/Shared/FuelStore.swift index b7d2cba..69b89c1 100644 --- a/Shared/FuelStore.swift +++ b/Shared/FuelStore.swift @@ -144,6 +144,7 @@ struct FuelStore { static let favouritesKey = "fuelboard.favourites" // [FuelStation] JSON static let alertsEnabledKey = "fuelboard.alertsEnabled" // Bool static let alertsRadiusKey = "fuelboard.alertsRadius" // Double km + static let lastRefreshKey = "fuelboard.lastRefresh" // TimeInterval (seconds since 1970) // MARK: Stations @@ -280,6 +281,29 @@ struct FuelStore { saveString(String(radius), service: alertsRadiusKey) } + // MARK: Refresh policy — data is cached; the app only auto-refreshes + // twice a day (pull-to-refresh is the manual override). + + static let refreshInterval: TimeInterval = 12 * 60 * 60 + + static func loadLastRefresh() -> Date? { + if let raw = loadString(service: lastRefreshKey), let ts = TimeInterval(raw) { + return Date(timeIntervalSince1970: ts) + } + return nil + } + + static func saveLastRefresh(_ date: Date = Date()) { + saveString(String(date.timeIntervalSince1970), service: lastRefreshKey) + } + + /// True when the cached data is fresh enough that a scheduled auto-refresh + /// should be skipped (twice-a-day policy). + static var isCacheFresh: Bool { + guard let last = loadLastRefresh() else { return false } + return Date().timeIntervalSince(last) < refreshInterval + } + // MARK: Low-level keychain helpers private static func keychainData(service: String) -> Data? {