Show the saving in the listing capsule on its own line
Station and Favourite rows share one capsule, so both now read
"Save / £3.10" and "Costs / £1.20 more" instead of the amount-free
"Saves" / "Costs more". "(approx.)" is appended on its own line when the
journey behind the figure was estimated. Break even carries no figure —
±£0.05 is noise — so it stays a single line unless the qualifier adds one.
- Split `listingCapsuleHeadline()` into a structured `ListingVerdict`
(headline / amount / qualifier) drawn as one Text per line, so where the
capsule breaks is decided by the data rather than by the width the price
column happened to hand it: the figure can never straddle two lines.
- The capsule's shape follows its line count: one line stays a Capsule, two
or more switch to a rounded rect at 8pt, whose corners do not crowd a
taller block the way a pill's semicircular ends do.
- The Key sheet's legend renders the same component with `showsAmount:
false`, so it demonstrates the wording without displaying the placeholder
£1.00 figure it was built from.
- The new words ("Save", "Costs", "(approx.)", "%@ more") go into
Localizable.strings so the copy editor owns them; amounts run through
FuelSavingsFormat.money and the amount line is monospaced.
- VoiceOver keeps the single-line form, since a drawn break is a pause
mid-phrase when spoken.
Tests cover the line structure for all four statuses, the estimated
qualifier and the amount-free legend. Pixel measurement on the simulator
confirms the shape rule: one line measures a radius of h/2 (a pill), two
lines measure 8pt, in both light and dark.
This commit is contained in:
@@ -263,18 +263,73 @@ extension FuelSavingsResult {
|
||||
headline(compact: true)
|
||||
}
|
||||
|
||||
/// Short, amount-free verdict used only by station listing rows. The detail
|
||||
/// sheet continues to use `capsuleHeadline()` so its full monetary verdict is
|
||||
/// unchanged. Confidence follows the existing journey provenance exactly.
|
||||
func listingCapsuleHeadline() -> String {
|
||||
let approximate = journeyConfidence.isEstimated ? " (approx.)" : ""
|
||||
/// The station-listing capsule's verdict, split into the lines the capsule
|
||||
/// draws.
|
||||
///
|
||||
/// The amount always takes its OWN line, so where the capsule breaks is
|
||||
/// decided by the data, never by the width it was handed: the figure can
|
||||
/// never straddle two lines, and a taller verdict grows the capsule downward
|
||||
/// instead of widening the row's fixed price column.
|
||||
struct ListingVerdict: Equatable {
|
||||
/// "Save", "Costs", "Break even" or "Closest station".
|
||||
let headline: String
|
||||
/// "£3.10" / "£1.20 more" — nil when there is no figure to show.
|
||||
let amount: String?
|
||||
/// "(approx.)" when the journey behind the figure was estimated.
|
||||
let qualifier: String?
|
||||
|
||||
var lines: [String] { [headline, amount, qualifier].compactMap { $0 } }
|
||||
|
||||
/// Single-line form, for VoiceOver and tests. A drawn newline is a pause
|
||||
/// in the middle of a phrase when spoken.
|
||||
var spokenText: String { lines.joined(separator: " ") }
|
||||
}
|
||||
|
||||
/// Verdict for the station listing rows — Stations and Favourites share one
|
||||
/// row, so they share this. `showsAmount: false` is the Key sheet's legend,
|
||||
/// which demonstrates the wording without inventing a figure.
|
||||
func listingVerdict(showsAmount: Bool = true) -> ListingVerdict {
|
||||
let qualifier = journeyConfidence.isEstimated ? String(localized: "(approx.)") : nil
|
||||
switch status {
|
||||
case .worthTheDrive: return "Saves\(approximate)"
|
||||
case .notWorthTheDrive: return "Costs more\(approximate)"
|
||||
case .breakEven: return "Break even\(approximate)"
|
||||
case .baseline: return Self.baselineHeadline
|
||||
case .worthTheDrive:
|
||||
return ListingVerdict(
|
||||
headline: String(localized: "Save"),
|
||||
amount: showsAmount ? FuelSavingsFormat.money(netSavingGBP) : nil,
|
||||
qualifier: qualifier
|
||||
)
|
||||
case .notWorthTheDrive:
|
||||
return ListingVerdict(
|
||||
headline: String(localized: "Costs"),
|
||||
amount: showsAmount ? Self.amountWithMore(abs(netSavingGBP)) : nil,
|
||||
qualifier: qualifier
|
||||
)
|
||||
case .breakEven:
|
||||
// No figure: ±£0.05 is noise, so this stays a single line unless the
|
||||
// estimate qualifier adds its own.
|
||||
return ListingVerdict(
|
||||
headline: String(localized: "Break even"),
|
||||
amount: nil,
|
||||
qualifier: qualifier
|
||||
)
|
||||
case .baseline:
|
||||
return ListingVerdict(
|
||||
headline: Self.baselineHeadline,
|
||||
amount: nil,
|
||||
qualifier: nil
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// "£1.20 more" — kept as a format string so the copy editor owns the word.
|
||||
private static func amountWithMore(_ value: Double) -> String {
|
||||
String(format: String(localized: "%@ more"), FuelSavingsFormat.money(value))
|
||||
}
|
||||
|
||||
/// One-line verdict for voice and tests; the capsule itself renders
|
||||
/// `listingVerdict()`'s lines separately.
|
||||
func listingCapsuleHeadline() -> String {
|
||||
listingVerdict().spokenText
|
||||
}
|
||||
}
|
||||
|
||||
/// One station's journey distance, plus how it was obtained.
|
||||
|
||||
Reference in New Issue
Block a user