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:
FuelBoard Contributor
2026-08-12 20:11:42 +01:00
parent 1dcd47c6cd
commit 04b233bf9a
8 changed files with 362 additions and 0 deletions
+32
View File
@@ -0,0 +1,32 @@
// FuelBoardLiveActivity.swift Live Activity shared model.
//
// Compiled into BOTH the app target (to start/update/end the activity) and
// the widget extension (to render it). ActivityKit is iOS-only, so this file
// is deliberately NOT symlinked into FuelBoardTests/Sources the SPM test
// target runs on macOS where ActivityKit does not exist.
//
// The activity mirrors the app's TOP badge: the cheapest station selling the
// app's selected fuel within the app's chosen distance radius. Tapping the
// activity opens Apple Maps driving directions to that station (maps://).
import ActivityKit
import Foundation
/// Attributes are fixed at request time. Here they pin the FUEL being
/// tracked; everything that can change while driving (station, price,
/// distance) lives in ContentState so updates don't need a new activity.
struct FuelBoardLiveActivityAttributes: ActivityAttributes {
var fuel: FuelType
public struct ContentState: Codable, Hashable {
var stationID: String
var stationName: String
var brand: String
/// Price in pence per litre (same unit as `FuelStation.prices`).
var pricePence: Double
var distanceKM: Double
var lat: Double
var lng: Double
var updatedAt: Date
}
}
+11
View File
@@ -262,6 +262,7 @@ struct FuelStore {
static let alertsRadiusKey = "fuelboard.alertsRadius" // Double km
static let alertsFuelKey = "fuelboard.alertsFuel" // FuelType raw value
static let debugModeKey = "fuelboard.debugMode" // Bool hidden dev flag
static let liveActivityKey = "fuelboard.liveActivity" // Bool Live Activity toggle
static let onboardingCompletedKey = "fuelboard.onboardingCompleted" // Bool
static let lastRefreshKey = "fuelboard.lastRefresh" // TimeInterval (seconds since 1970)
static let relaySourceKey = "fuelboard.relaySource" // String "api" | "csv"
@@ -490,6 +491,16 @@ struct FuelStore {
saveString(String(radius), service: alertsRadiusKey)
}
// MARK: Live Activity
static func loadLiveActivityEnabled() -> Bool {
loadString(service: liveActivityKey) == "1"
}
static func saveLiveActivityEnabled(_ enabled: Bool) {
saveString(enabled ? "1" : "0", service: liveActivityKey)
}
// MARK: Refresh policy data is cached; the app only auto-refreshes
// twice a day (pull-to-refresh is the manual override).