Station detail: one verdict capsule, the chosen price style, and the working explained
The Best Value card states its verdict once. The badge is now the large capsule form the sheet used to repeat on its own line, so the same headline no longer appears twice, and the comparison caption ("vs closest station" / "your baseline") sits under the capsule — on the card and on the listing rows alike. The row's size-class split, which put the caption beside the capsule at regular width, is gone, so the pair reads the same on every device.
The facts row's price now follows Settings -> Units -> Price display. It called FuelStore.priceText(price, style: .stationSign) with the style hardcoded, so a Pounds & pence user still saw the bare forecourt figure under the map. It now uses priceTextAttributed with the style passed down from the list that opened the sheet (ContentView, StationsView, FavouritesView) — the same call the listing row makes, at the same @ScaledMetric(relativeTo: .title2) 22pt so it still scales with Dynamic Type.
"How Best Value works" gains a step-by-step section in plain English: the baseline and why it is the closest station by journey, the return trip and why the extra return travel can read larger than the one-way distance, the extra driving cost, the fuel-price saving, the total and its inversion when the drive costs more than the price saves, and where the minimum saving is set. Every statement matches the arithmetic in FuelSavingsCalculator. The sheet opens at 72% instead of 42% to give it room.
No calculation, ranking, wording or data source changed: comparisonLabel, priceTextAttributed and the shared FuelSavingsResult are the same calls they were.
This commit is contained in:
@@ -835,6 +835,7 @@ struct ContentView: View {
|
||||
station: station,
|
||||
location: location,
|
||||
distanceUnit: distanceUnit,
|
||||
priceStyle: priceDisplayStyle,
|
||||
fuelSavings: savingsLookup(baseline: scaffold.baseline,
|
||||
journeys: scaffold.journeys)(station),
|
||||
fuelSavingsAmount: sessionFuelSavingsAmount,
|
||||
@@ -2091,27 +2092,22 @@ struct StationRow: View {
|
||||
|
||||
/// The savings capsule plus the comparison it is measured against. The
|
||||
/// capsule itself is the shared verdict view, so a row, the detail sheet and
|
||||
/// the compact summary cannot drift apart. The comparison label sits beside
|
||||
/// the capsule only where there is width for both.
|
||||
/// the compact summary cannot drift apart. The comparison label sits UNDER
|
||||
/// the capsule, trailing-aligned with it, at every width — beside it, a long
|
||||
/// verdict pushed the caption off the row and the pair no longer read as one
|
||||
/// statement.
|
||||
@ViewBuilder
|
||||
private func savingsCapsule(_ result: FuelSavingsResult) -> some View {
|
||||
let comparison = Text(FuelSavingsFormat.comparisonLabel(status: result.status))
|
||||
VStack(alignment: .trailing, spacing: 2) {
|
||||
FuelSavingsCapsule(result: result)
|
||||
Text(FuelSavingsFormat.comparisonLabel(status: result.status))
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
// Same narrow column as the capsule above it, so the same floor:
|
||||
// scaled copy, not a trimmed "vs closest st…".
|
||||
.minimumScaleFactor(0.7)
|
||||
if sizeClass == .regular {
|
||||
HStack(spacing: 8) {
|
||||
FuelSavingsCapsule(result: result)
|
||||
comparison
|
||||
}
|
||||
} else {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
FuelSavingsCapsule(result: result)
|
||||
comparison
|
||||
}
|
||||
.multilineTextAlignment(.trailing)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -211,6 +211,7 @@ struct FavouritesView: View {
|
||||
station: station,
|
||||
location: location,
|
||||
distanceUnit: distanceUnit,
|
||||
priceStyle: priceDisplayStyle,
|
||||
selectedFuel: activeFuel,
|
||||
fuelSavings: savingsForStation(station),
|
||||
fuelSavingsAmount: fuelSavingsAmount,
|
||||
|
||||
@@ -16,6 +16,34 @@ struct HowBestValueSheet: View {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text("Best Value compares each station with the closest station by journey distance. It works out what you could save at the pump, then takes off the fuel used for the extra return journey.")
|
||||
Text("Your fill-up amount and MPG are used to show whether you could save overall.")
|
||||
}
|
||||
.font(.subheadline)
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
|
||||
Section("The working, step by step") {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
step("1. The baseline",
|
||||
"The closest station by journey distance. It is the trip you would make anyway, so everything else is measured against it.")
|
||||
step("2. The return trip",
|
||||
"For each station we work out the journey there and back. Where the roads have been measured we use the real driving route; until then we double the straight-line distance and mark the figure “Approx.”.")
|
||||
step("3. Extra return travel",
|
||||
"This station's return trip minus the baseline's. It is the extra you add on top of the journey you were already making — not the whole trip. Because both figures are return trips, it can read larger than the one-way distance shown above the map.")
|
||||
step("4. Extra driving cost",
|
||||
"What that extra distance costs in fuel: the extra miles, divided by your MPG, priced at the station you are driving to.")
|
||||
step("5. Fuel-price saving",
|
||||
"What the difference in price per litre is worth on your fill-up.")
|
||||
step("6. Total",
|
||||
"The fuel-price saving minus the extra driving cost. If the drive costs more than the price saves, the total is shown as an extra cost instead, and the station is not Best Value.")
|
||||
step("7. Best Value",
|
||||
"The total clears the minimum saving you set in Settings → Fuel Savings.")
|
||||
}
|
||||
.font(.subheadline)
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
|
||||
Section {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text("With a £ budget you spend that amount at each station; the litres differ by station. Savings compare those litres with the same quantity at your closest station, then deduct extra journey fuel. You do not receive cash back.")
|
||||
Text("With Litres, every station is compared for the fixed quantity you selected.")
|
||||
Text("Results are marked “Approx.” until road distances are available.")
|
||||
@@ -33,9 +61,20 @@ struct HowBestValueSheet: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.presentationDetents([.fraction(0.42)])
|
||||
.presentationDetents([.fraction(0.72)])
|
||||
.presentationBackground(Color(UIColor.systemGroupedBackground))
|
||||
}
|
||||
|
||||
/// One numbered step: a lead-in, then the plain explanation. Same words
|
||||
/// either way — this only sets the hierarchy.
|
||||
private func step(_ title: String, _ explanation: String) -> some View {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(title)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
Text(explanation)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The row that opens `HowBestValueSheet`. A tappable row with a chevron, like
|
||||
|
||||
@@ -14,6 +14,15 @@ struct StationFactsRow: View {
|
||||
let priceAge: FuelPriceFreshness.Assessment
|
||||
let distance: StationDistanceDisplay?
|
||||
let distanceUnit: DistanceUnit
|
||||
/// The user's price display style, from the same Settings choice the listing
|
||||
/// rows and the widgets read.
|
||||
var priceStyle: PriceDisplayStyle = FuelStore.loadPriceDisplayStyle()
|
||||
|
||||
/// The price's point size, scaled with Dynamic Type: the price is built from
|
||||
/// an explicit size (its "/L" suffix is sized relative to it inside
|
||||
/// `priceTextAttributed`), so it needs a metric rather than a font style.
|
||||
/// 22pt matches the listing row's price.
|
||||
@ScaledMetric(relativeTo: .title2) private var priceSize: CGFloat = 22
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
@@ -29,9 +38,7 @@ struct StationFactsRow: View {
|
||||
Text(selectedFuel.displayName)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
Text(priceText)
|
||||
.font(.title2.weight(.bold))
|
||||
.monospacedDigit()
|
||||
priceView
|
||||
StationPriceAgeView(assessment: priceAge, compact: true)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
@@ -55,12 +62,23 @@ struct StationFactsRow: View {
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
|
||||
/// The selected grade's price, exactly as the listing row prints it. A
|
||||
/// missing grade shows an em dash rather than hiding the column, so the row
|
||||
/// The selected grade's price, in the user's chosen display style — the same
|
||||
/// `priceTextAttributed` the listing row uses, so the sheet prints the bare
|
||||
/// forecourt figure ("167.9") in Station sign and the superscripted
|
||||
/// "£1.67⁹/L" in Pounds & pence, and the two surfaces cannot disagree.
|
||||
/// The returned Text carries its own fonts, so no `.font(...)` goes on top.
|
||||
///
|
||||
/// A missing grade shows an em dash rather than hiding the column, so the row
|
||||
/// keeps its two-column shape.
|
||||
private var priceText: String {
|
||||
guard let price = station.prices[selectedFuel] else { return "—" }
|
||||
return FuelStore.priceText(price, style: .stationSign)
|
||||
@ViewBuilder
|
||||
private var priceView: some View {
|
||||
if let price = station.prices[selectedFuel] {
|
||||
FuelStore.priceTextAttributed(price, style: priceStyle, size: priceSize, weight: .bold)
|
||||
} else {
|
||||
Text("—")
|
||||
.font(.title2.weight(.bold))
|
||||
.monospacedDigit()
|
||||
}
|
||||
}
|
||||
|
||||
/// One-way distance, with its confidence marker, from the shared display.
|
||||
@@ -114,33 +132,35 @@ struct BestValueDetailCard: View {
|
||||
private func availableBody(_ result: FuelSavingsResult) -> some View {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
header(result)
|
||||
verdict(result)
|
||||
panels(result)
|
||||
workings(result)
|
||||
}
|
||||
}
|
||||
|
||||
/// Card title on the left, the verdict badge on the right — the same capsule
|
||||
/// the listing rows carry, so the badge and the large capsule below it can
|
||||
/// never disagree.
|
||||
/// Card title on the left, the verdict on the right — ONE capsule, in the
|
||||
/// same large form the sheet used to repeat on its own line above the
|
||||
/// working. The comparison caption sits UNDER the capsule, trailing-aligned
|
||||
/// with it, the same way the listing rows lay the pair out.
|
||||
private func header(_ result: FuelSavingsResult) -> some View {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 10) {
|
||||
Text("Best Value")
|
||||
.font(.headline)
|
||||
Spacer(minLength: 8)
|
||||
FuelSavingsCapsule(result: result)
|
||||
VStack(alignment: .trailing, spacing: 4) {
|
||||
FuelSavingsCapsule(result: result, compact: false)
|
||||
.accessibilityLabel(result.spokenHeadline())
|
||||
comparison(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The verdict capsule keeps its own line above the working, as before.
|
||||
private func verdict(_ result: FuelSavingsResult) -> some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
FuelSavingsCapsule(result: result, compact: false)
|
||||
.accessibilityLabel(result.spokenHeadline())
|
||||
/// The compact comparison caption ("vs closest station" / "your baseline"),
|
||||
/// under the capsule it belongs to.
|
||||
private func comparison(_ result: FuelSavingsResult) -> some View {
|
||||
Text(FuelSavingsFormat.comparisonLabel(status: result.status))
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.multilineTextAlignment(.trailing)
|
||||
}
|
||||
|
||||
/// The two figures the estimate is made OF, side by side: what the user
|
||||
|
||||
@@ -17,6 +17,9 @@ struct StationDetailView: View {
|
||||
let station: FuelStation
|
||||
let location: Coordinate?
|
||||
let distanceUnit: DistanceUnit
|
||||
/// Price display style, from the same Settings choice the listing rows read,
|
||||
/// so the sheet's price can never disagree with the row behind it.
|
||||
var priceStyle: PriceDisplayStyle = FuelStore.loadPriceDisplayStyle()
|
||||
/// Fuel whose price is being shown by the surrounding station list.
|
||||
var selectedFuel: FuelType = FuelStore.loadSelectedFuel()
|
||||
var fuelSavings: FuelSavingsAvailability? = nil
|
||||
@@ -244,7 +247,8 @@ struct StationDetailView: View {
|
||||
selectedFuel: selectedFuel,
|
||||
priceAge: priceFreshness,
|
||||
distance: distanceDisplay,
|
||||
distanceUnit: distanceUnit
|
||||
distanceUnit: distanceUnit,
|
||||
priceStyle: priceStyle
|
||||
)
|
||||
ForEach(StationNotice.notices(for: station)) { notice in
|
||||
Label(notice.title, systemImage: notice.symbolName)
|
||||
|
||||
@@ -254,6 +254,7 @@ struct StationsView: View {
|
||||
station: station,
|
||||
location: location,
|
||||
distanceUnit: distanceUnit,
|
||||
priceStyle: priceDisplayStyle,
|
||||
selectedFuel: selectedFuel,
|
||||
fuelSavings: savingsForStation(station),
|
||||
fuelSavingsAmount: fuelSavingsAmount,
|
||||
|
||||
Reference in New Issue
Block a user