Alerts tab: picker-style config matching Live Activity; move Live Activity there

- Alerts section restyled to mirror the Live Activity section: one section
  with toggle + Fuel picker + Radius picker (was segmented fuel + slider)
- Alert radius options expanded to 1/2/3/5/8/10/15/20 (display unit, snapped
  to nearest option; stored km unchanged; validation widened to 33 km)
- Live Activity section moved from Settings into the Alerts tab so both
  'notify while driving' surfaces sit together
- SettingsView loses the Live Activity bindings/section
This commit is contained in:
FuelBoard Contributor
2026-08-12 22:30:54 +01:00
parent a721ccd187
commit 7f7c0fff03
4 changed files with 66 additions and 74 deletions
+57 -35
View File
@@ -3,59 +3,63 @@ import SwiftUI
/// Alerts tab enables the cheapest-station proximity alerts and shows what's
/// being monitored. The fuel being monitored is chosen HERE, independently of
/// the Stations-tab selection: users can browse any fuel without re-targeting
/// their alerts (and vice versa).
/// their alerts (and vice versa). The Live Activity section also lives here
/// it mirrors this page's shape (toggle + fuel + distance) so the two
/// "notify me while driving" surfaces sit together.
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 the "cheapest nearby" Live Activity is shown on the Lock Screen
/// / Dynamic Island. Tracks its OWN fuel + radius (below) independent of
/// the Stations tab.
@Binding var liveActivityEnabled: Bool
/// Fuel the Live Activity tracks (independent of the Stations tab).
@Binding var liveActivityFuel: FuelType
/// Search radius (miles, 5/10/15) the Live Activity uses.
@Binding var liveActivityRadiusMiles: Int
let distanceUnit: DistanceUnit
let monitoredCount: Int
let lastAlert: String?
/// The radius slider works in the user's chosen unit; the stored value
/// stays km so ProximityMonitor and persistence never change.
private var radiusInUnit: Double { distanceUnit.fromKM(radius) }
/// 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> {
Binding(
get: {
let inUnit = distanceUnit.fromKM(radius)
return FuelStore.alertRadiusOptions
.min(by: { abs(Double($0) - inUnit) < abs(Double($1) - inUnit) }) ?? 3
},
set: { value in
radius = distanceUnit.toKM(Double(value))
}
)
}
var body: some View {
NavigationStack {
List {
Section {
Toggle("Cheapest-station alerts", isOn: $enabled)
Picker("Fuel", selection: $fuel) {
ForEach(FuelType.allCases) { fuel in
Text(fuel.displayName).tag(fuel)
}
}
Picker("Radius", selection: radiusOption) {
ForEach(FuelStore.alertRadiusOptions, id: \.self) { value in
Text("\(value) \(distanceUnit.label)").tag(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.")
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.")
}
if enabled {
Section {
Picker("Fuel", selection: $fuel) {
ForEach(FuelType.allCases) { fuel in
Text(fuel.shortName).tag(fuel)
}
}
.pickerStyle(.segmented)
} header: {
Text("Fuel to monitor")
} footer: {
Text("Alerts watch for the cheapest \(fuel.displayName.lowercased()) station within the radius.")
}
Section("Trigger radius") {
VStack(alignment: .leading, spacing: 8) {
HStack {
Text("Radius")
Spacer()
Text(String(format: "%.1f %@", radiusInUnit, distanceUnit.shortName))
.foregroundStyle(.secondary)
.monospacedDigit()
}
// 110 in the user's unit; convert back to km on change.
Slider(value: Binding(
get: { radiusInUnit },
set: { radius = distanceUnit.toKM($0) }
), in: 1...10, step: distanceUnit == .kilometers ? 1 : 0.5)
}
}
Section("Monitoring") {
if monitoredCount == 0 {
Text("No stations monitored yet — open the Stations tab to load prices first.")
@@ -79,6 +83,24 @@ struct AlertsView: View {
}
}
}
Section {
Toggle("Live Activity", isOn: $liveActivityEnabled)
Picker("Fuel", selection: $liveActivityFuel) {
ForEach(FuelType.allCases) { fuel in
Text(fuel.displayName).tag(fuel)
}
}
Picker("Distance", selection: $liveActivityRadiusMiles) {
ForEach(FuelStore.stationRadiusOptions, id: \.self) { miles in
Text("\(miles) \(distanceUnit.label)").tag(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.")
}
}
.navigationTitle("Alerts")
}
+3 -3
View File
@@ -159,6 +159,9 @@ struct ContentView: View {
enabled: $alertsEnabled,
radius: $alertsRadius,
fuel: $alertsFuel,
liveActivityEnabled: $liveActivityEnabled,
liveActivityFuel: $liveActivityFuel,
liveActivityRadiusMiles: $liveActivityRadiusMiles,
distanceUnit: distanceUnit,
monitoredCount: monitor.monitoredStationIDs.count,
lastAlert: monitor.lastAlert
@@ -325,9 +328,6 @@ struct ContentView: View {
private var settingsTab: some View {
SettingsView(
distanceUnit: $distanceUnit,
liveActivityEnabled: $liveActivityEnabled,
liveActivityFuel: $liveActivityFuel,
liveActivityRadiusMiles: $liveActivityRadiusMiles,
alertsFuel: alertsFuel,
alertsRadiusKM: alertsRadius,
testAlertResult: monitor.lastTestResult,
-35
View File
@@ -12,14 +12,6 @@ import WidgetKit
/// with no criteria at all.
struct SettingsView: View {
@Binding var distanceUnit: DistanceUnit
/// 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.
@Binding var liveActivityEnabled: Bool
/// Fuel the Live Activity tracks (independent of the Stations tab).
@Binding var liveActivityFuel: FuelType
/// Search radius (miles, 5/10/15) the Live Activity uses.
@Binding var liveActivityRadiusMiles: Int
/// The fuel + radius currently configured for alerts (mirrors the Alerts
/// tab) so the test notification matches what real alerts will say.
var alertsFuel: FuelType = .e10
@@ -74,33 +66,6 @@ struct SettingsView: View {
Text("Distances and search radii across the app, widget and alerts are shown in this unit.")
}
Section {
Toggle("Live Activity", isOn: $liveActivityEnabled)
Picker("Fuel", selection: $liveActivityFuel) {
ForEach(FuelType.allCases) { fuel in
Text(fuel.displayName).tag(fuel)
}
}
.onChange(of: liveActivityFuel) { _, newValue in
FuelStore.saveLiveActivityFuel(newValue)
}
Picker("Distance", selection: $liveActivityRadiusMiles) {
ForEach(FuelStore.stationRadiusOptions, id: \.self) { miles in
Text("\(miles) \(distanceUnit.label)").tag(miles)
}
}
.onChange(of: liveActivityRadiusMiles) { _, newValue in
FuelStore.saveLiveActivityRadiusMiles(newValue)
}
} 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.")
}
.onChange(of: liveActivityEnabled) { _, newValue in
FuelStore.saveLiveActivityEnabled(newValue)
}
Section {
Button {
onShowOnboarding()
+6 -1
View File
@@ -482,8 +482,13 @@ struct FuelStore {
saveString(enabled ? "1" : "0", service: alertsEnabledKey)
}
/// Alert trigger-radius options (in the user's display unit, mapped to km
/// on selection). Expanded beyond the Live Activity's 5/10/15 so approach
/// alerts can trigger close (13) or wide (up to 20).
static let alertRadiusOptions = [1, 2, 3, 5, 8, 10, 15, 20]
static func loadAlertsRadius() -> Double {
if let raw = loadString(service: alertsRadiusKey), let value = Double(raw), value >= 1, value <= 10 {
if let raw = loadString(service: alertsRadiusKey), let value = Double(raw), value >= 1, value <= 33 {
return value
}
return 3.0