Distance confidence - Every distance surface takes the figure AND its "Approx." marker from one FuelStore.stationDistanceDisplay call (station rows, detail sheet, widget, Live Activity, watch, Siri), so a surface can never mark one figure approximate while showing a routed one. Radius filters and Closest ordering test that same displayed distance, so no row can read a distance outside the radius that filtered it (the reported "14.6 mi inside a 10-mile list"). Accurate distances (Settings -> Distances, default off) - Off: the browse layer serves the straight line, marked Approx., for every row, so two installs agree to the metre and the app makes no route requests at all. - On: routed distances for the whole search area, unmarked, served only from a COMPLETE set anchored within 600 m — a partial fill reverts every row at once rather than mixing road and straight-line bases. - Best Value maths sits deliberately outside the toggle: it keeps using measured road routes and its own confidence marking, so money figures never move with the switch. - Fills run only while the toggle is on and the app is frontmost; capacity raised to 600 stations per pass; completeness tolerates pins Apple Maps has no route for, which otherwise left an area permanently incomplete and the whole list stuck on estimates. Performance (the reported slowdown switching to Closest / launching with it) - FuelStore.accurateDistancesEnabled was a UserDefaults(suiteName:) read per call: a synchronous cfprefsd round-trip measured at ~1.9 us, ~200k calls per Closest sort. Memoised, with the setter keeping it in step. - Both keyed sorts decorate their keys — one distance per station, carried as an index instead of copying whole stations into the decorated array — and the comparison baseline plus per-station journeys now come from ONE pass instead of two full scans per tab. The Stations tab also reuses its single sorted list for the count and the TOP badge instead of sorting the country three times. - Measured on iPhone SE (7,914-station Closest pool): sort 36.6 ms -> 17.1 ms per list, Cheapest 6 ms, Best Value 11 ms. - DEBUG probes: FB_PERF list timing, FB_PERF_BENCH toggle-read benchmark, and FB_DIST_FOCUS now reports the served value, marker and mode (and re-prints when a fill lands). FB_ACCURATE_DISTANCES / FB_SORT_MODE env forms for devices, where devicectl eats a leading-dash argument. Tests: 198, 0 failures. Device and simulator Debug builds green.
130 lines
5.1 KiB
Swift
130 lines
5.1 KiB
Swift
import ActivityKit
|
|
import BackgroundTasks
|
|
import SwiftUI
|
|
|
|
@main
|
|
struct FuelBoardApp: App {
|
|
init() {
|
|
WatchSyncManager.shared.activate()
|
|
#if DEBUG
|
|
// QA hook (Debug builds only): `-qaLiveActivity e10|e5|diesel` starts a
|
|
// Live Activity with a long station name so the Lock Screen / island
|
|
// layout can be rendered in the Simulator for visual QA.
|
|
let args = ProcessInfo.processInfo.arguments
|
|
if let idx = args.firstIndex(of: "-qaLiveActivity"),
|
|
args.indices.contains(idx + 1),
|
|
let fuel = FuelType(rawValue: args[idx + 1]) {
|
|
startQALiveActivity(fuel: fuel)
|
|
}
|
|
#endif
|
|
}
|
|
|
|
var body: some Scene {
|
|
WindowGroup {
|
|
ContentView()
|
|
.onOpenURL(perform: handleOpenURL)
|
|
.onReceive(NotificationCenter.default.publisher(for: .fuelBoardWatchRefreshRequested)) { _ in
|
|
WatchSyncManager.shared.pushSnapshot()
|
|
}
|
|
}
|
|
.backgroundTask(.appRefresh(SmartDataRefreshScheduler.taskIdentifier)) {
|
|
await SmartDataRefreshCoordinator.runBackgroundProbe()
|
|
}
|
|
}
|
|
|
|
#if DEBUG
|
|
private func startQALiveActivity(fuel: FuelType) {
|
|
let state = FuelBoardLiveActivityAttributes.ContentState(
|
|
fuel: fuel,
|
|
stationID: "qa-phoenix",
|
|
stationName: "Phoenix Filling Stations",
|
|
brand: "Phoenix",
|
|
pricePence: 1499,
|
|
priceDisplayStyle: FuelStore.loadPriceDisplayStyle(),
|
|
distanceKM: 8.0,
|
|
// QA mock: marked approximate so the hook renders the LONGEST
|
|
// distance form ("Approx. 8.0 mi") and the island / Lock Screen
|
|
// layout can be checked against it.
|
|
distanceIsApproximate: true,
|
|
lat: 51.5,
|
|
lng: -0.12,
|
|
updatedAt: Date()
|
|
)
|
|
let attrs = FuelBoardLiveActivityAttributes()
|
|
do {
|
|
let activity = try Activity.request(
|
|
attributes: attrs,
|
|
content: .init(state: state, staleDate: nil),
|
|
pushType: nil
|
|
)
|
|
print("QA-LIVE-ACTIVITY STARTED id=\(activity.id)")
|
|
} catch {
|
|
print("QA-LIVE-ACTIVITY FAILED: \(error)")
|
|
}
|
|
}
|
|
#endif
|
|
|
|
/// Handles deep links that end up in the app. Widget taps arrive here in
|
|
/// two cases:
|
|
/// - legacy/cached widget timelines using the `fuelboard://` relay, or
|
|
/// - the Home Screen delivering a `maps://` URL to the containing app
|
|
/// instead of opening Maps directly (the system's choice on iOS).
|
|
/// Either way we forward to the native Apple Maps directions URL. The
|
|
/// `http://maps.apple.com` form is also handled, from very old timelines.
|
|
/// In CarPlay the widget uses the same `maps://` URL, which routes to
|
|
/// Apple Maps without ever launching this app.
|
|
/// A `fuelboard://station?stationID=…&lat=…&lng=…&name=…` link (from a
|
|
/// widget or Live Activity tap when Settings → Tap station = More info)
|
|
/// posts a notification that ContentView observes to present the detail
|
|
/// sheet (falling back to the map sheet when the station has left
|
|
/// the cache).
|
|
private func handleOpenURL(_ url: URL) {
|
|
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
|
|
else { return }
|
|
|
|
if components.scheme == "fuelboard",
|
|
components.host == "station" {
|
|
let items = components.queryItems ?? []
|
|
var userInfo: [String: Any] = [:]
|
|
if let id = items.first(where: { $0.name == "stationID" })?.value {
|
|
userInfo["stationID"] = id
|
|
}
|
|
if let lat = items.first(where: { $0.name == "lat" })?.value.flatMap(Double.init) {
|
|
userInfo["stationLat"] = lat
|
|
}
|
|
if let lng = items.first(where: { $0.name == "lng" })?.value.flatMap(Double.init) {
|
|
userInfo["stationLng"] = lng
|
|
}
|
|
if let name = items.first(where: { $0.name == "name" })?.value {
|
|
userInfo["stationName"] = name
|
|
}
|
|
NotificationCenter.default.post(
|
|
name: .fuelBoardShowStationDetail,
|
|
object: nil,
|
|
userInfo: userInfo
|
|
)
|
|
return
|
|
}
|
|
|
|
guard let items = components.queryItems
|
|
else { return }
|
|
|
|
// Preferred: maps://?daddr=lat,lng&t=d (also covers http://maps.apple.com).
|
|
if let daddr = items.first(where: { $0.name == "daddr" })?.value {
|
|
openMaps(daddr: daddr)
|
|
return
|
|
}
|
|
|
|
// Legacy relay: fuelboard://directions?lat=..&lng=..
|
|
if let lat = items.first(where: { $0.name == "lat" })?.value.flatMap(Double.init),
|
|
let lng = items.first(where: { $0.name == "lng" })?.value.flatMap(Double.init) {
|
|
openMaps(daddr: "\(lat),\(lng)")
|
|
}
|
|
}
|
|
|
|
private func openMaps(daddr: String) {
|
|
guard let mapsURL = URL(string: "maps://?daddr=\(daddr)&t=d") else { return }
|
|
UIApplication.shared.open(mapsURL)
|
|
}
|
|
}
|