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
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -14,6 +14,7 @@ struct StationsView: View {
|
||||
let radiusKM: Double
|
||||
let favouriteIDs: Set<String>
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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? {
|
||||
|
||||
Reference in New Issue
Block a user