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:
@@ -65,6 +65,10 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca
|
||||
/// Debug-only snapshot for the Settings → Debug section. Recomputed on
|
||||
/// every location/stations change via `refreshDebugStatus()`.
|
||||
@Published private(set) var debugStatus: DebugLocationStatus?
|
||||
/// Last CoreLocation region-monitoring failure, if any. Shown in Settings
|
||||
/// → Debug so a silently-failed `startMonitoring` (region budget exceeded,
|
||||
/// auth not granted, radius too large) is visible instead of invisible.
|
||||
@Published private(set) var lastRegionError: String?
|
||||
|
||||
private let manager = CLLocationManager()
|
||||
private var stations: [FuelStation] = []
|
||||
@@ -117,6 +121,10 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca
|
||||
|
||||
guard enabled else { return }
|
||||
|
||||
// A successful re-registration pass resets any earlier failure so the
|
||||
// Debug section only shows the CURRENT region problem.
|
||||
lastRegionError = nil
|
||||
|
||||
// Favourites first (guaranteed slots), then closest stations, max 18.
|
||||
// `self.favourites` is already filtered to the monitored fuel.
|
||||
var candidates: [FuelStation] = self.favourites
|
||||
@@ -198,10 +206,14 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca
|
||||
|
||||
private func requestPermissions() {
|
||||
// Region monitoring needs Always location for background delivery.
|
||||
// Never request WhenInUse and Always back-to-back — iOS ignores the
|
||||
// second call while the first prompt is pending, leaving the app
|
||||
// stuck on WhenInUse and background geofence entries undelivered.
|
||||
// Escalation happens in locationManagerDidChangeAuthorization:
|
||||
// WhenInUse granted -> request Always -> re-register on grant.
|
||||
switch manager.authorizationStatus {
|
||||
case .notDetermined:
|
||||
manager.requestWhenInUseAuthorization()
|
||||
manager.requestAlwaysAuthorization()
|
||||
case .authorizedWhenInUse:
|
||||
manager.requestAlwaysAuthorization()
|
||||
default:
|
||||
@@ -210,6 +222,25 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca
|
||||
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { _, _ in }
|
||||
}
|
||||
|
||||
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
|
||||
switch manager.authorizationStatus {
|
||||
case .authorizedWhenInUse:
|
||||
// Escalate: background region delivery (the whole point of
|
||||
// alerts) requires Always. iOS shows the upgrade prompt here.
|
||||
manager.requestAlwaysAuthorization()
|
||||
case .authorizedAlways:
|
||||
// Regions registered under WhenInUse-only won't deliver in the
|
||||
// background; re-register now that Always is granted. (ContentView's
|
||||
// LocationManager separately restarts significant-change tracking
|
||||
// on its own delegate callback, so the region window follows the
|
||||
// user in the background.)
|
||||
update(stations: stations, favourites: FuelStore.loadFavourites(),
|
||||
fuel: fuel, radiusKM: radiusKM)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Region events
|
||||
|
||||
nonisolated func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
|
||||
@@ -222,6 +253,20 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca
|
||||
// Ignore — alerts still work while the app is open.
|
||||
}
|
||||
|
||||
nonisolated func locationManager(
|
||||
_ manager: CLLocationManager,
|
||||
monitoringDidFailFor region: CLRegion?,
|
||||
withError error: Error
|
||||
) {
|
||||
Task { @MainActor in
|
||||
// Surface region-registration failures instead of swallowing
|
||||
// them: a failed startMonitoring (region budget, auth, radius)
|
||||
// means alerts silently stop. Cleared on the next successful
|
||||
// re-registration (update() resets it).
|
||||
lastRegionError = "\(region?.identifier ?? "?") · \(error.localizedDescription)"
|
||||
}
|
||||
}
|
||||
|
||||
private func handleEntry(_ region: CLRegion) {
|
||||
let stationID = region.identifier
|
||||
// Fall back to cached data on background wake (stations may not be
|
||||
|
||||
Reference in New Issue
Block a user