Two columns split by a 1pt hairline: identity on the left at a fixed 52%
share so rows line up when scrolling, price and savings on the right,
leading-aligned on one axis with the price dominant. The favourites star
sits under the station logo, freeing the width the pounds-and-pence price
needs to render in full rather than scaled or truncated.
Badges stay tinted rather than filled — BEST VALUE green, CHEAPEST blue,
none for nearest — and the verdict capsule is one shared view across the
listing, the detail sheet and the compact summary, so wording and colour
cannot drift between them.
Best Value's minimum saving moves from Alerts to Settings, Fuel Savings,
beside the fill size and MPG that produce the saving; the Alerts tab reads
it back instead of offering it twice. Its options are now 50p steps to £5
(was 50p/£1/£2/£5), so £2.50 and £3 are reachable, with a test pinning the
step, the ceiling and the store's string round-trip.
The verdict capsule scales to 0.7 before truncating. Measured on a 393pt
screen its copy is handed ~69pt while "Closest station" needs ~90pt, so the
old 0.8 floor could not reach it and rows showed "Closest stati..." instead.
Verified: Debug build clean, swift test 249/249.
The relay's price_updated is the maximum price_last_updated across the grades it maps, so it cannot vouch for the selected grade: stop using it to score, gate or warn about Best Value.
- FuelPriceFreshness is now station-level and display-only: one age per station, fresh/outdated/unknown/invalid, "May be outdated" past 30 days, with future-tolerance and non-finite guards.
- Best Value restores the closest-station-selling-the-selected-fuel baseline and its savings ranking; eligibility gating, eligible-first sorting and the unavailable banner are gone.
- Price age appears in the More info sheet only - no listing-row warnings, no extra banners.
- Copy reverted from "closest eligible station" to "closest station"; stale price-confidence strings removed, "May be outdated" added.
- Dormant per-grade timestamp plumbing removed, and regression tests added for every fuel type, missing/invalid/future stamps and the bundled offline dump.
- Station detail now separates the station's own report age from the
selected fuel's update time, so a recent station-wide report no longer
reads as an old price.
- Warning copy names the fuel and says which side is weak: the station's
price is not individually dated, or the baseline's is. The misleading
"unverified or older than 7 days" line is gone.
- Wording stays "Closest" throughout; new copy uses Unleaded / Premium /
Diesel rather than the internal grade codes.
- Localisation: new and updated keys for the added copy.
The More info row went inside the Best Value card's `if let fuelSavings`, but the
Key sheet row was left ungated, so it stayed visible with Best Value switched off.
It is now gated on `fuelSavingsSettings.isConfigured` — exactly the condition
`savingsForStation` uses, i.e. the same gate as the card — so the two rows appear
and disappear together.
Verified on the simulator by toggling Best Value in Settings and scrolling the Key
sheet to its end (four drags, sheet still up and its footer in view each time):
- OFF: the row is absent at every scroll position, and the More info sheet shows
neither the card nor the row;
- ON: the row is present, and the More info card and its row are both back.
210 tests pass; Release builds.
The explanation used to be a section inside the Key sheet. It is a standalone
HowBestValueSheet now, opened from a tappable row with a chevron: at the bottom of
the Key sheet, and at the bottom of the More info sheet below the Best Value card.
Gating: the More info row sits INSIDE the same `if let fuelSavings` as the card it
follows, so it disappears with the feature. Verified with Best Value toggled off in
Settings: the More info sheet then shows neither the card nor the row. The Key
sheet row is deliberately ungated — that sheet is the legend and is reachable
whatever the feature is doing.
Copy is unchanged: the same Localizable.strings strings are reused, and the old
section header became the sheet's title. A DEBUG-only `-showBestValueInfo` hook
opens the sheet without a tap.
The third sort segment, the caption line it drives on the Stations tab, and the
Settings sentence about sorting now read "Best Value" — that is
SortMode.worthTheDrive.displayName plus the two copy sites that name the sort.
The feature that backs it keeps the name "Worth the Drive": the Settings toggle
("Enable Worth the Drive"), the Fuel Savings section header and footer, the sheet
header "Worth the drive?", the "Worth the Drive unavailable: …" text and
FuelSavingsStatus.worthTheDrive's own wording are all unchanged.
The enum's raw value stays `worthTheDrive`, so stored sort preferences and widget
configs keep decoding — only the label moved.
3 new tests pin the three sort names, the stability of the raw values, and the
fact that the feature's own wording did not change (172 total, 0 failures).
Verified on the simulator: the Stations segmented control reads
Closest | Cheapest | Best Value.
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.
Calculation audit (brief config: 30 MPG imperial, £60 fill-up, baseline
Rontec Shelf 171.9p at 0.4 mi):
- One mile<->kilometre constant pair (kilometresPerMile 1.609344,
metresPerMile 1609.344) replaces the mixed 1609.34 / 0.621371 / 1.60934
used in the savings path. Effect ~2 ppm (invisible), but the conversions
are now auditable in one place.
- New FuelSavingsCalculationAuditTests (20 deterministic tests, no live
MKDirections) pins the brief's three stations - Tesco 170.9p/2.0 mi,
MFG Hare & Hounds 172.9p/2.8 mi, PGG Napoleon 171.9p/2.8 mi - plus
imperial-vs-US gallon, MPG and budget scaling, the break-even tolerance
boundary, the nearer-candidate clamp, route-unavailable, mixed-basis
estimates and display-rounding boundaries.
- Formula unchanged, and verified correct: fixed quantity (budget / baseline
price), pump saving, round-trip extra distance clamped at zero, travel fuel
priced at the candidate station, +/-5p break even, baseline labelled
"Closest station", sorting on the unrounded net saving. The brief's "£0.18"
for Tesco is the rounded 1 dp extra-distance caption (2.02-2.05 mi reads as
"2.0 mi"), not a rounding bug in the money.
UI (uncommitted since d7a0ecb):
- Done bar drawn in the bottom safe area from keyboard notifications:
.toolbar(placement: .keyboard) is device-dependent (present on the 15 Pro,
absent on the SE on the same iOS build).
- Favourite star in the More-info heading, wired to the same fuel-scoped
toggle as the listing rows at all three call sites.
- CHEAPEST/TOP badge moved above the price so it reads as part of it.
- Listing rows carry the real session fill-up amount instead of a hard zero
(the compact banner does not print it today; a future row layout would).
153 tests, 0 failures. Simulator and device builds succeed.
Optional Fuel Savings feature (off by default): vehicle MPG in UK imperial gallons, fill amount in pounds or litres, persisted through the existing keychain/app-group store. Ranks the WHOLE station pool by estimated net saving (fuel-price saving at each pump minus the extra return-trip driving cost) using MapKit road distances with a straight-line fallback marked (est.).
The closest station with a valid price for the selected fuel is the comparison baseline and is labelled "Closest station"; other rows read "Save £X.XX after travel" / "£X.XX worse off after travel" / "Break even". Labels live in the shared model so they are unit-tested. The TOP badge still marks the cheapest pump price and reads "CHEAPEST" when the list is ranked by net saving.
Road routing now prioritises the Worth-the-Drive candidates plus the baseline and merges cache entries instead of replacing them, so far-out stations get real routes rather than permanent straight-line estimates. Numeric fields gain a Done button above the keypad (decimalPad has no Return key); the fill-up control is hidden unless the feature is configured; Favourites shares one session fill amount with the station list. 131 tests.
Settings → Units gains a Tap station picker: Open map (default,
historic behaviour — tap opens Apple Maps directions) or More info
(tap shows a detail sheet with name, address, distance from current
location, and a Directions button). Applies to Stations and
Favourites tabs; persisted keychain-first as
fuelboard.stationTapAction. Stations footer hint follows the setting.
Move FuelType.tintColor (green=unleaded #30D158, yellow=premium #FFD60A,
cyan=diesel #64D2FF) into Shared/FuelStore.swift so the app, widget, and
Live Activity all read one definition (was app-only in StationsView, so the
widget couldn't see it). Tint the Live Activity rich card pump icon and the
Dynamic Island compactLeading pump by context.state.fuel.tintColor.
Sheet grows from .medium to .fraction(0.6) so the footer ('Colours
match each station's rating on the list') clears the bottom edge with
comfortable margin (verified: footer clearance +33pt). Sheet background
switched from the translucent default to a solid
systemGroupedBackground via .presentationBackground so the stations
list no longer shows through.
The two bottom cards (TOP, star) previously had no padding — their
background hugged the text. Add .padding(.horizontal, 14).padding(.vertical, 12),
matching (slightly exceeding) the RAG cards' vertical padding so the
info cards read as proper cards.
Price-rating legend now renders as five accent cards (tip-section style):
Best value (green, checkmark) / Okay (orange, equal) / Pricey (red,
exclamation) each with a RAG dot + threshold (≤1.5p / ≤4p / >4p) on the
right in the row's accent colour; TOP (blue capsule) and star (yellow)
as accent cards below a divider. Footer: 'Colours match each station's
rating on the list'. Copy split into title/blurb keys in Localizable.
Sheet chrome (medium detent, Done, info button) unchanged.
The N/TOTAL stations-updated explainer now sits in a plain caption Section
cell at the bottom of the tab (font .caption, secondary) — matching the
Favourites tab's bottom callout instead of rendering as a section footer.
The header section was removed entirely — the tab now opens with Fuel type.
The explainer lives as a section footer at the bottom of the tab:
- Cheapest: 'Cheapest Unleaded within 5 miles — 46/8022 stations updated ·
tap a station for directions.'
- Closest: 'Closest Unleaded stations — nearest first, best value within
5 miles · 46/8022 stations updated'
- no-location variant keeps mode + tap-for-directions hint
Numerator = stations in the current list pool (as before); denominator =
full UK station total from the last chain fetch (loadStationCount, 8,022
mirror snapshot) — ratio hidden until the first fetch lands.
- FuelTypeSegmentedPicker promoted to shared (was private in StationsView)
and parameterised by fuel list so Favourites passes only fuels with favourites
- Favourites tab swaps native .segmented picker for the capsule control with
green/purple/grey fuelpump icons, matching Stations
Fixing the bug where favouriting an Unleaded entry also created a Diesel
favourite. Favourites are now [FavouriteEntry] = (station, fuel) pairs:
- Starring a row pins it only for the fuel being viewed
- Favourites tabs show only fuels that actually have favourites
- Geofence monitor gives priority slots only to favourites matching the
monitored fuel
- Tests updated + new fuel-scoping test (31/31 passing)
The 'N stations within X' caption now lives in the header explainer
(Closest/Cheapest + radius + count on one line); the Distance section
shows just the picker.
- FuelType labels: 'Unleaded (E10)'/'Premium (E5)' -> 'Unleaded'/'Premium'
- New DistanceUnit (miles/km) preference; all app + widget distances,
radii and alert text convert/display in the chosen unit
- Settings tab (4th tab): segmented miles/km picker, 'Show introduction'
replay button (onboarding moved out of Alerts tab), StoreKit tip £4.99
- ProximityMonitor + widget notifications use the chosen unit
- Tests: label expectations updated, DistanceUnit conversion + format tests