Fuel Savings: MPG and fill-up are sliders, not typed fields

The two Fuel Savings figures and the in-list fill-up control were text
fields. A typed figure could sit outside the slider's own bounds, so the
control and the number it claimed to show could disagree, and the decimal
pad needed a Done button to escape.

- Settings: "Average fuel economy" and "Typical fill-up" are now sliders
  with a read-out ("45 MPG", "£40" / "40 L") in place of the field. The
  warning copy reads "Set a realistic..." rather than "Enter...".
- FuelSavingsAdjustmentView (shared by the Stations Worth-the-Drive header
  and the Favourites tab) drops its text field and gains the same read-out;
  the typed-value machinery and the -focusFillAmount QA hook go with it.
- Slider bounds live in FuelSavingsCalculator (fillAmountSliderRange/Step,
  mpgSliderRange/Step) so the Settings control and the in-list control can
  never offer different ranges, and the fill-up slider range stays inside
  the calculator's validity ceiling (asserted by test).
- Two tests pin those invariants: every slider position is a valid amount,
  and both MPG bounds are valid MPG values. 155 tests, 0 failures.
- Comments that described the removed fields were updated; the keyboard
  Done-bar plumbing is kept (no numeric field remains in the app, so it can
  no longer fire) rather than deleted, so any field added later still has an
  escape route.

Verified on the simulator: Settings shows "Average fuel economy / 45 MPG"
with no field, and the Worth-the-Drive fill-up control shows the unit picker
with a "£40" read-out and a slider. The Settings "Typical fill-up" row sits
below the fold on every simulator available here (and synthetic input cannot
scroll it), so those pixels are unverified.
This commit is contained in:
FuelBoard Contributor
2026-10-11 17:41:53 +01:00
parent d0a89d97d5
commit cf3abb3fa7
5 changed files with 116 additions and 65 deletions
+35 -15
View File
@@ -71,9 +71,10 @@ struct SettingsView: View {
@State private var mediumWidgetDiag: String?
@State private var installedWidgets: String = ""
/// Which numeric field in the Fuel Savings section is being edited. Those
/// fields use `.decimalPad`, which has no Return key, so the keyboard's
/// Done button clears this to dismiss AND commit the typed value.
/// Which numeric field on this screen is being edited. `.decimalPad` has no
/// Return key, so the keyboard's Done button clears this to dismiss AND
/// commit the typed value. Both Fuel Savings controls are sliders now, so
/// nothing sets it — it stays for any numeric field this screen takes again.
@FocusState private var focusedSavingsField: SavingsField?
private enum SavingsField: Hashable {
@@ -153,14 +154,15 @@ struct SettingsView: View {
HStack {
Text("Average fuel economy")
Spacer()
TextField("45", value: fuelSavingsMPGBinding, format: .number.precision(.fractionLength(0...1)))
.keyboardType(.decimalPad)
.multilineTextAlignment(.trailing)
.frame(width: 80)
.focused($focusedSavingsField, equals: .averageMPG)
Text("MPG")
Text(mpgText)
.monospacedDigit()
.foregroundStyle(.secondary)
}
Slider(value: fuelSavingsMPGBinding,
in: FuelSavingsCalculator.mpgSliderRange,
step: FuelSavingsCalculator.mpgSliderStep)
.accessibilityLabel("Average fuel economy")
.accessibilityValue(mpgText)
Picker("Fill-up unit", selection: fuelSavingsUnitBinding) {
ForEach(FuelSavingsFillUnit.allCases) { unit in
Text(unit.displayName).tag(unit)
@@ -170,14 +172,17 @@ struct SettingsView: View {
HStack {
Text("Typical fill-up")
Spacer()
TextField("40", value: fuelSavingsAmountBinding, format: .number.precision(.fractionLength(0...1)))
.keyboardType(.decimalPad)
.multilineTextAlignment(.trailing)
.frame(width: 90)
.focused($focusedSavingsField, equals: .fillAmount)
Text(fillUpText)
.monospacedDigit()
.foregroundStyle(.secondary)
}
Slider(value: fuelSavingsAmountBinding,
in: FuelSavingsCalculator.fillAmountSliderRange(for: fuelSavingsSettings.preferredAmount.unit),
step: FuelSavingsCalculator.fillAmountSliderStep(for: fuelSavingsSettings.preferredAmount.unit))
.accessibilityLabel("Typical fill-up")
.accessibilityValue(fillUpText)
if !fuelSavingsSettings.isConfigured && fuelSavingsSettings.enabled {
Text("Enter a realistic UK MPG and fill-up amount before Worth the Drive appears in sorting.")
Text("Set a realistic UK MPG and fill-up amount before Worth the Drive appears in sorting.")
.font(.caption)
.foregroundStyle(.orange)
}
@@ -655,6 +660,21 @@ struct SettingsView: View {
)
}
/// "45 MPG" / "45.5 MPG". The slider is the only control, so this read-out
/// is the one place the figure is stated.
private var mpgText: String {
let mpg = fuelSavingsSettings.mpg
return mpg.rounded() == mpg ? String(format: "%.0f MPG", mpg) : String(format: "%.1f MPG", mpg)
}
/// "£40" / "40 L" — the fill-up slider's read-out.
private var fillUpText: String {
switch fuelSavingsSettings.preferredAmount.unit {
case .pounds: return String(format: "£%.0f", fuelSavingsSettings.preferredAmount.value)
case .litres: return String(format: "%.0f L", fuelSavingsSettings.preferredAmount.value)
}
}
private func persistFuelSavingsSettings() {
FuelStore.saveFuelSavingsSettings(fuelSavingsSettings)
}