Live Activity: cheapest-in-radius Lock Screen/Dynamic Island pill
- Shared FuelBoardLiveActivityAttributes/ContentState (app + widget targets) - FuelBoardLiveActivity Widget: ActivityConfiguration rendered by the widget extension - Lock Screen banner + Dynamic Island (leading/trailing/bottom, compact, minimal), tap anywhere -> maps:// directions to the best station - LiveActivityManager (app side): request/update/end; mirrors the app list's TOP badge (selected fuel + chosen distance radius, price-then-distance ties); local start-date tracking for the 8h cap (this SDK's Activity has no startDate) - auto-restarts at 7h59m; silent no-op when disabled/denied - Wiring: 250m/60s location hook (incl. background significant-change wake-ups), refresh completion, fuel/radius/unit changes, toggle flip - Settings toggle + FuelStore persistence (keychain-backed, app-group) - App-Info.plist: NSSupportsLiveActivities
This commit is contained in:
@@ -14,6 +14,7 @@ struct ContentView: View {
|
||||
@State private var alertsEnabled: Bool = FuelStore.loadAlertsEnabled()
|
||||
@State private var alertsRadius: Double = FuelStore.loadAlertsRadius()
|
||||
@State private var alertsFuel: FuelType = FuelStore.loadAlertsFuel()
|
||||
@State private var liveActivityEnabled: Bool = FuelStore.loadLiveActivityEnabled()
|
||||
@State private var location: Coordinate? = {
|
||||
if let loc = FuelStore.loadLocation() { return Coordinate(lat: loc.lat, lng: loc.lng) }
|
||||
return nil
|
||||
@@ -188,10 +189,15 @@ struct ContentView: View {
|
||||
locationManager.onLocationUpdate = { [weak monitor] in
|
||||
monitor?.update(stations: stations, favourites: refreshedFavourites,
|
||||
fuel: alertsFuel, radiusKM: alertsRadius)
|
||||
// The Live Activity follows the same wake-ups — this hook
|
||||
// fires on every fix incl. background significant-change
|
||||
// wake-ups, so the Lock Screen pill stays live while driving.
|
||||
updateLiveActivity()
|
||||
}
|
||||
monitor.update(stations: stations, favourites: refreshedFavourites,
|
||||
fuel: alertsFuel, radiusKM: alertsRadius)
|
||||
monitor.setEnabled(alertsEnabled)
|
||||
updateLiveActivity()
|
||||
// Refresh only when the cache is stale (twice-a-day policy).
|
||||
Task { await refresh() }
|
||||
} else {
|
||||
@@ -210,6 +216,7 @@ struct ContentView: View {
|
||||
locationManager.startForegroundTracking()
|
||||
monitor.update(stations: stations, favourites: refreshedFavourites,
|
||||
fuel: alertsFuel, radiusKM: alertsRadius)
|
||||
updateLiveActivity()
|
||||
Task { await refresh(force: true) }
|
||||
}
|
||||
}
|
||||
@@ -223,6 +230,7 @@ struct ContentView: View {
|
||||
}
|
||||
monitor.update(stations: stations, favourites: refreshedFavourites,
|
||||
fuel: alertsFuel, radiusKM: alertsRadius)
|
||||
updateLiveActivity()
|
||||
// No network fetch on foreground — pull-to-refresh is the override.
|
||||
} else {
|
||||
locationManager.stopForegroundTracking()
|
||||
@@ -237,11 +245,13 @@ struct ContentView: View {
|
||||
// NOT re-fetched on every movement (cached, twice-a-day policy).
|
||||
monitor.update(stations: stations, favourites: refreshedFavourites,
|
||||
fuel: alertsFuel, radiusKM: alertsRadius)
|
||||
updateLiveActivity()
|
||||
}
|
||||
}
|
||||
.onChange(of: selectedFuel) { _, _ in
|
||||
// No re-fetch needed — one response carries E5/E10/DIESEL prices.
|
||||
WidgetCenter.shared.reloadAllTimelines()
|
||||
updateLiveActivity()
|
||||
}
|
||||
.onChange(of: stationLimit) { _, newValue in
|
||||
// Distance filter is LOCAL math now — the cache holds the full-UK
|
||||
@@ -250,6 +260,7 @@ struct ContentView: View {
|
||||
// next render.
|
||||
FuelStore.saveStationLimit(newValue)
|
||||
WidgetCenter.shared.reloadAllTimelines()
|
||||
updateLiveActivity()
|
||||
}
|
||||
.onChange(of: alertsEnabled) { _, newValue in
|
||||
FuelStore.saveAlertsEnabled(newValue)
|
||||
@@ -272,6 +283,31 @@ struct ContentView: View {
|
||||
monitor.update(stations: stations, favourites: refreshedFavourites,
|
||||
fuel: newValue, radiusKM: alertsRadius)
|
||||
}
|
||||
.onChange(of: liveActivityEnabled) { _, newValue in
|
||||
// Toggling the Live Activity on starts it with the current best
|
||||
// station; toggling off ends any running activity.
|
||||
FuelStore.saveLiveActivityEnabled(newValue)
|
||||
updateLiveActivity()
|
||||
}
|
||||
.onChange(of: distanceUnit) { _, _ in
|
||||
// Distance unit changes the radius — mirror the new radius in the
|
||||
// Live Activity immediately.
|
||||
updateLiveActivity()
|
||||
}
|
||||
}
|
||||
|
||||
/// Pushes the current best-in-radius station into the Live Activity.
|
||||
/// No-op (or ends the activity) when the toggle is off or there's no
|
||||
/// location/data yet. Mirrors the app list's TOP badge: selected fuel +
|
||||
/// chosen distance radius, cheapest then nearest on ties.
|
||||
private func updateLiveActivity() {
|
||||
LiveActivityManager.update(
|
||||
stations: stations,
|
||||
fuel: selectedFuel,
|
||||
radiusKM: distanceUnit.toKM(Double(stationLimit)),
|
||||
location: location,
|
||||
enabled: liveActivityEnabled
|
||||
)
|
||||
}
|
||||
|
||||
/// The Settings tab, extracted from `body` so the TabView expression stays
|
||||
@@ -279,6 +315,7 @@ struct ContentView: View {
|
||||
private var settingsTab: some View {
|
||||
SettingsView(
|
||||
distanceUnit: $distanceUnit,
|
||||
liveActivityEnabled: $liveActivityEnabled,
|
||||
alertsFuel: alertsFuel,
|
||||
alertsRadiusKM: alertsRadius,
|
||||
testAlertResult: monitor.lastTestResult,
|
||||
@@ -342,6 +379,8 @@ struct ContentView: View {
|
||||
// Keep monitor geofences in sync with the freshest data.
|
||||
monitor.update(stations: stations, favourites: refreshedFavourites,
|
||||
fuel: alertsFuel, radiusKM: alertsRadius)
|
||||
// Fresh prices → refresh the Live Activity pill too.
|
||||
updateLiveActivity()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user