Fix widget distance menu + geofence notification chain

Widget distance menu: the Distance picker only makes sense for
Cheapest ordering. It was hidden for Favourites but Closest still
showed it (regressed when the per-widget Distance picker was added).
parameterSummary now nests When clauses: Distance visible for
Cheapest only; Closest and Favourites hide it.

Notifications: three compounding defects kept real geofence alerts
from ever firing while the app was suspended:

1. Always permission was never properly requested. requestPermissions
   fired WhenInUse and Always back-to-back; iOS ignores the second
   call while the first prompt is pending, leaving the app stuck on
   WhenInUse — and region entries are never delivered in the
   background. Added locationManagerDidChangeAuthorization to
   ProximityMonitor: escalate WhenInUse -> Always, and re-register
   geofences on grant (regions registered under WhenInUse-only won't
   deliver in the background).

2. The 18-region window was frozen in the background. Re-registration
   lived in SwiftUI .onChange(of: locationManager.current), which
   never runs while suspended. Added a delegate hook
   (LocationManager.onLocationUpdate) fired from didUpdateLocations on
   every fix including background significant-change wake-ups; the app
   wires it to re-register geofences around the new position.

3. Region-registration failures were invisible. monitoringDidFailFor
   was never implemented, so a failed startMonitoring (region budget,
   auth, radius) silently stopped alerts. Now surfaced as
   monitor.lastRegionError and shown in Settings -> Debug; cleared on
   the next successful registration.
This commit is contained in:
FuelBoard Contributor
2026-08-12 18:18:24 +01:00
parent f822bd2636
commit 1dcd47c6cd
4 changed files with 93 additions and 11 deletions
+22 -1
View File
@@ -179,6 +179,16 @@ struct ContentView: View {
// network fetch start once it's done.
if FuelStore.loadHasCompletedOnboarding() {
locationManager.startForegroundTracking()
// Geofences must follow the user even in the background:
// wire the delegate hook (fires on every fix incl. background
// significant-change wake-ups) to re-register the region
// window around the new position. SwiftUI onChange alone
// never runs while suspended, so alerts would otherwise stay
// frozen around the last foreground fix.
locationManager.onLocationUpdate = { [weak monitor] in
monitor?.update(stations: stations, favourites: refreshedFavourites,
fuel: alertsFuel, radiusKM: alertsRadius)
}
monitor.update(stations: stations, favourites: refreshedFavourites,
fuel: alertsFuel, radiusKM: alertsRadius)
monitor.setEnabled(alertsEnabled)
@@ -277,7 +287,8 @@ struct ContentView: View {
onRefreshDebugStatus: { monitor.refreshDebugStatus() },
onShowOnboarding: { showOnboarding = true },
debugStatus: monitor.debugStatus,
appCheapest: appCheapestStatus
appCheapest: appCheapestStatus,
regionError: monitor.lastRegionError
)
.tabItem { Label("Settings", systemImage: "gearshape.fill") }
}
@@ -457,6 +468,12 @@ struct StationRow: View {
@MainActor
final class LocationManager: NSObject, ObservableObject, @preconcurrency CLLocationManagerDelegate {
@Published var current: Coordinate?
/// Called after every location fix (foreground AND background wake-ups).
/// The app uses it to re-register geofences around the new position
/// SwiftUI's `.onChange` never runs in the background, so this delegate
/// hook is the only path that keeps the 18-region window following the
/// user while driving with the app suspended.
var onLocationUpdate: (() -> Void)?
private let manager = CLLocationManager()
override init() {
@@ -522,6 +539,10 @@ final class LocationManager: NSObject, ObservableObject, @preconcurrency CLLocat
lastWidgetReload = now
WidgetCenter.shared.reloadAllTimelines()
}
// Re-register geofences around the new position. Also fires on
// background significant-change wake-ups, which SwiftUI onChange
// never sees this is what keeps alerts working while driving.
onLocationUpdate?()
}
private var lastWidgetReload = Date.distantPast