Alerts: mile-friendly radius 1/2/3/5/8 + Follow search (capped 8 mi); Live Activity Follow search; legacy radii clamped

This commit is contained in:
FuelBoard Contributor
2026-08-13 15:00:53 +01:00
parent bb48995fac
commit 490c343fd5
5 changed files with 210 additions and 27 deletions
+63 -10
View File
@@ -10,6 +10,8 @@ struct AlertsView: View {
@Binding var enabled: Bool
@Binding var radius: Double // stored in km (monitor + storage)
@Binding var fuel: FuelType // the fuel alerts monitor for
/// Whether alerts mirror the Stations-tab search distance (capped at 8 mi).
@Binding var followsSearch: Bool
/// Whether the "cheapest nearby" Live Activity is shown on the Lock Screen
/// / Dynamic Island. Tracks its OWN fuel + radius (below) independent of
/// the Stations tab.
@@ -18,22 +20,71 @@ struct AlertsView: View {
@Binding var liveActivityFuel: FuelType
/// Search radius (miles, 5/10/15) the Live Activity uses.
@Binding var liveActivityRadiusMiles: Int
/// Whether the Live Activity mirrors the Stations-tab search distance.
@Binding var liveActivityFollowsSearch: Bool
/// Current Stations-tab search distance (miles) the value "Follow search"
/// mirrors.
let stationLimit: Int
let distanceUnit: DistanceUnit
let monitoredCount: Int
let lastAlert: String?
/// The Stations-tab search distance shown in the user's unit, e.g.
/// "5 miles" or "8 km" for the "Follow search" rows.
private var searchInUnitLabel: String {
let km = Double(stationLimit) * 1.60934 // stationLimit is always miles
return "\(Int(distanceUnit.fromKM(km).rounded())) \(distanceUnit.label)"
}
private enum AlertRadiusChoice: Hashable {
case followSearch
case fixed(Int)
}
/// The radius picker works in the user's chosen unit; the stored value
/// stays km so ProximityMonitor and persistence never change. Options are
/// snapped to the nearest picker value so any stored radius still selects.
private var radiusOption: Binding<Int> {
/// "Follow search" mirrors the Stations-tab distance (capped at 8 miles).
private var alertChoice: Binding<AlertRadiusChoice> {
Binding(
get: {
if followsSearch { return .followSearch }
let inUnit = distanceUnit.fromKM(radius)
return FuelStore.alertRadiusOptions
let snapped = FuelStore.alertRadiusOptions
.min(by: { abs(Double($0) - inUnit) < abs(Double($1) - inUnit) }) ?? 3
return .fixed(snapped)
},
set: { value in
radius = distanceUnit.toKM(Double(value))
set: { choice in
switch choice {
case .followSearch:
followsSearch = true
case .fixed(let value):
followsSearch = false
radius = distanceUnit.toKM(Double(value))
}
}
)
}
private enum LADistanceChoice: Hashable {
case followSearch
case fixed(Int)
}
private var laChoice: Binding<LADistanceChoice> {
Binding(
get: {
if liveActivityFollowsSearch { return .followSearch }
return .fixed(liveActivityRadiusMiles)
},
set: { choice in
switch choice {
case .followSearch:
liveActivityFollowsSearch = true
case .fixed(let miles):
liveActivityFollowsSearch = false
liveActivityRadiusMiles = miles
}
}
)
}
@@ -48,15 +99,16 @@ struct AlertsView: View {
Text(fuel.displayName).tag(fuel)
}
}
Picker("Radius", selection: radiusOption) {
Picker("Radius", selection: alertChoice) {
Text("Follow search (\(searchInUnitLabel))").tag(AlertRadiusChoice.followSearch)
ForEach(FuelStore.alertRadiusOptions, id: \.self) { value in
Text("\(value) \(distanceUnit.label)").tag(value)
Text("\\(value) \\(distanceUnit.label)").tag(AlertRadiusChoice.fixed(value))
}
}
} header: {
Text("Cheapest-station alerts")
} footer: {
Text("When you approach a station that is the cheapest within the radius, FuelBoard sends a notification — even with the app closed. Alerts watch for the cheapest \(fuel.displayName.lowercased()) station within the radius.")
Text("When you approach a station that is the cheapest within the radius, FuelBoard sends a notification — even with the app closed. Alerts watch for the cheapest \\(fuel.displayName.lowercased()) station within the radius. Follow search mirrors the Stations-tab distance, capped at 8 miles for reliable geofencing.")
}
if enabled {
@@ -91,15 +143,16 @@ struct AlertsView: View {
Text(fuel.displayName).tag(fuel)
}
}
Picker("Distance", selection: $liveActivityRadiusMiles) {
Picker("Distance", selection: laChoice) {
Text("Follow search (\(searchInUnitLabel))").tag(LADistanceChoice.followSearch)
ForEach(FuelStore.stationRadiusOptions, id: \.self) { miles in
Text("\(miles) \(distanceUnit.label)").tag(miles)
Text("\\(miles) \\(distanceUnit.label)").tag(LADistanceChoice.fixed(miles))
}
}
} header: {
Text("Live Activity")
} footer: {
Text("Shows the cheapest station for the fuel and distance you pick here on the Lock Screen and Dynamic Island — independent of the Stations tab. Updates as you drive; tap to open directions. Live Activities can be disabled in System Settings → Live Activities.")
Text("Shows the cheapest station for the fuel and distance you pick here on the Lock Screen and Dynamic Island — independent of the Stations tab (choose Follow search to mirror its distance instead). Updates as you drive; tap to open directions. Live Activities can be disabled in System Settings → Live Activities.")
}
}
.navigationTitle("Alerts")