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:
@@ -4,14 +4,23 @@ struct FuelSavingsAdjustmentView: View {
|
||||
@Binding var amount: FuelSavingsFillAmount
|
||||
let referencePricePencePerLitre: Double?
|
||||
|
||||
@State private var textValue: String = ""
|
||||
/// Focus for the numeric field — also lets the DEBUG QA hook below raise the
|
||||
/// keyboard on launch so the Done bar can be screenshotted (a script cannot
|
||||
/// tap the simulator).
|
||||
@FocusState private var isAmountFieldFocused: Bool
|
||||
|
||||
private var range: ClosedRange<Double> {
|
||||
amount.unit == .pounds ? 10...100 : 10...80
|
||||
FuelSavingsCalculator.fillAmountSliderRange(for: amount.unit)
|
||||
}
|
||||
|
||||
private var step: Double {
|
||||
FuelSavingsCalculator.fillAmountSliderStep(for: amount.unit)
|
||||
}
|
||||
|
||||
/// Read-out of the figure the slider is on. Deliberately NOT a text field:
|
||||
/// a typed figure could sit outside the slider's own bounds, leaving the
|
||||
/// control and the value it claimed to show disagreeing. Slider-only means
|
||||
/// what you see is the figure the estimate actually uses.
|
||||
private var valueText: String {
|
||||
switch amount.unit {
|
||||
case .pounds: return String(format: "£%.0f", amount.value)
|
||||
case .litres: return String(format: "%.0f L", amount.value)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -25,33 +34,17 @@ struct FuelSavingsAdjustmentView: View {
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
HStack {
|
||||
Slider(value: sliderBinding, in: range, step: amount.unit == .pounds ? 5 : 1)
|
||||
TextField(amount.unit == .pounds ? "£" : "Litres", text: valueBinding)
|
||||
.keyboardType(.decimalPad)
|
||||
.multilineTextAlignment(.trailing)
|
||||
.frame(width: 72)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.focused($isAmountFieldFocused)
|
||||
.accessibilityIdentifier("fillAmountField")
|
||||
Slider(value: sliderBinding, in: range, step: step)
|
||||
.accessibilityLabel("Fill-up amount")
|
||||
.accessibilityValue(valueText)
|
||||
Text(valueText)
|
||||
.monospacedDigit()
|
||||
.frame(width: 72, alignment: .trailing)
|
||||
}
|
||||
Text("Changes the estimate for this session only — your Settings default stays unchanged.")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.onAppear {
|
||||
syncText()
|
||||
#if DEBUG
|
||||
// QA hook: raise the keyboard without a tap so the Done bar can be
|
||||
// screenshotted on a simulator (synthetic taps do not reach it).
|
||||
if ProcessInfo.processInfo.arguments.contains("-focusFillAmount") {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
||||
isAmountFieldFocused = true
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
.onChange(of: amount.value) { _, _ in syncText() }
|
||||
.onChange(of: amount.unit) { _, _ in syncText() }
|
||||
}
|
||||
|
||||
private var unitBinding: Binding<FuelSavingsFillUnit> {
|
||||
@@ -65,7 +58,6 @@ struct FuelSavingsAdjustmentView: View {
|
||||
} else {
|
||||
amount = FuelSavingsFillAmount(value: newUnit == .pounds ? 40 : 30, unit: newUnit)
|
||||
}
|
||||
syncText()
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -76,23 +68,6 @@ struct FuelSavingsAdjustmentView: View {
|
||||
set: { amount.value = min(max($0, range.lowerBound), range.upperBound) }
|
||||
)
|
||||
}
|
||||
|
||||
private var valueBinding: Binding<String> {
|
||||
Binding(
|
||||
get: { textValue },
|
||||
set: { raw in
|
||||
textValue = raw
|
||||
if let value = Double(raw), value > 0 {
|
||||
amount.value = min(value, FuelSavingsCalculator.maximumFillAmount(for: amount.unit))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private func syncText() {
|
||||
let value = amount.value
|
||||
textValue = value.rounded() == value ? String(format: "%.0f", value) : String(format: "%.1f", value)
|
||||
}
|
||||
}
|
||||
|
||||
struct FuelSavingsSummaryView: View {
|
||||
|
||||
Reference in New Issue
Block a user