Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93df94ddc9 | ||
|
|
78b43ce6d7 | ||
|
|
7dbb3d1a15 | ||
|
|
13967c3782 | ||
|
|
6e534ed5b7 |
@@ -0,0 +1,69 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// The "How Best Value works" explanation as its own sheet.
|
||||||
|
///
|
||||||
|
/// It used to sit inline in the Key sheet as a `Section`; it is one shared view
|
||||||
|
/// now so the Key sheet and the More info sheet can both open it, and the copy is
|
||||||
|
/// unchanged (the same `Localizable.strings` keys are reused — the old section
|
||||||
|
/// header string is the sheet's title).
|
||||||
|
struct HowBestValueSheet: View {
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationStack {
|
||||||
|
List {
|
||||||
|
Section {
|
||||||
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
|
Text("Best Value compares each station with the closest one. 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.")
|
||||||
|
Text("Results are marked “Approx.” until road distances are available.")
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
.font(.subheadline)
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("How Best Value works")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .confirmationAction) {
|
||||||
|
Button("Done") { dismiss() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.presentationDetents([.fraction(0.42)])
|
||||||
|
.presentationBackground(Color(UIColor.systemGroupedBackground))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The row that opens `HowBestValueSheet`. A tappable row with a chevron, like
|
||||||
|
/// the other rows in both sheets. `.buttonStyle(.plain)` keeps it from becoming a
|
||||||
|
/// row-wide tap target that could swallow a neighbouring control (see the
|
||||||
|
/// two-buttons-in-one-row rule).
|
||||||
|
struct HowBestValueRow: View {
|
||||||
|
let action: () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button(action: action) {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Text("How Best Value works")
|
||||||
|
Spacer(minLength: 8)
|
||||||
|
Image(systemName: "chevron.forward")
|
||||||
|
.font(.footnote.weight(.semibold))
|
||||||
|
.foregroundStyle(.tertiary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
/// QA hook support: `-showBestValueInfo` opens this sheet without a tap, so it can
|
||||||
|
/// be screenshotted (simctl has no tap command) — the same pattern as
|
||||||
|
/// `-showKeySheet` / `-showDetail`.
|
||||||
|
enum BestValueInfoQA {
|
||||||
|
static var isRequested: Bool {
|
||||||
|
ProcessInfo.processInfo.arguments.contains("-showBestValueInfo")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -33,6 +33,9 @@ struct StationDetailView: View {
|
|||||||
/// the caption fades out on its own.
|
/// the caption fades out on its own.
|
||||||
@State private var showingCopied = false
|
@State private var showingCopied = false
|
||||||
@State private var copyResetTask: Task<Void, Never>?
|
@State private var copyResetTask: Task<Void, Never>?
|
||||||
|
/// The "How Best Value works" sheet, opened from the row at the bottom of the
|
||||||
|
/// Best Value card (the Key sheet opens the same shared view).
|
||||||
|
@State private var showBestValueInfo = false
|
||||||
|
|
||||||
private let reportIssueURL = URL(string: "https://www.gov.uk/guidance/report-an-error-in-fuel-prices-or-forecourt-details")!
|
private let reportIssueURL = URL(string: "https://www.gov.uk/guidance/report-an-error-in-fuel-prices-or-forecourt-details")!
|
||||||
|
|
||||||
@@ -123,43 +126,37 @@ struct StationDetailView: View {
|
|||||||
// clutter. The Address/Distance and Distance/actions lines
|
// clutter. The Address/Distance and Distance/actions lines
|
||||||
// stay.
|
// stay.
|
||||||
.listRowSeparator(.hidden, edges: .bottom)
|
.listRowSeparator(.hidden, edges: .bottom)
|
||||||
// The map, the address, the distance, any notices and the two
|
// The map, the address, the distance, any notices and the two
|
||||||
// station actions all share ONE card. The list's own row
|
// station actions all share ONE card (the list's own row
|
||||||
// hairlines separate them (inset, the same line that already
|
// hairlines separate them), so no hand-rolled Divider here —
|
||||||
// separates Address from Distance), so no hand-rolled Divider is
|
// adding one would double the line.
|
||||||
// needed — adding one would double the line.
|
//
|
||||||
// The station name is NOT repeated here: the sheet header
|
// The station name is NOT repeated: the sheet header already
|
||||||
// (navigationTitle, inline) already shows it, and the address
|
// shows it.
|
||||||
// line below has any leading repeat of the name stripped, so
|
|
||||||
// a "Name" row only said it twice.
|
|
||||||
//
|
//
|
||||||
// The whole row is a copy control: it puts JUST the postcode
|
// The whole row is a copy control: it puts JUST the postcode
|
||||||
// on the clipboard so it can be pasted into another
|
// on the clipboard so it can be pasted into another
|
||||||
// navigation app. The copy glyph and its "Copied"
|
// navigation app. The copy glyph sits ON the address line,
|
||||||
// confirmation share a SECOND line that is always there, so
|
// and the "Copied" confirmation is a capsule that floats OVER
|
||||||
// the confirmation can never reflow the address above it.
|
// THIS ROW — it confirms the address, so it must not hover
|
||||||
|
// over the map. It is an overlay, so nothing moves while it is
|
||||||
|
// up, and `allowsHitTesting(false)` keeps the row tappable
|
||||||
|
// through the capsule.
|
||||||
Button {
|
Button {
|
||||||
copyPostcode()
|
copyPostcode()
|
||||||
} label: {
|
} label: {
|
||||||
VStack(alignment: .leading, spacing: 6) {
|
HStack(alignment: .firstTextBaseline, spacing: 8) {
|
||||||
HStack(alignment: .firstTextBaseline, spacing: 8) {
|
Text("Address")
|
||||||
Text("Address")
|
Spacer(minLength: 8)
|
||||||
Spacer(minLength: 8)
|
Text(addressLine)
|
||||||
Text(addressLine)
|
.multilineTextAlignment(.trailing)
|
||||||
.multilineTextAlignment(.trailing)
|
.foregroundStyle(.secondary)
|
||||||
.foregroundStyle(.secondary)
|
// Stays the plain copy glyph — the capsule carries
|
||||||
}
|
// the confirmation, so swapping this to a checkmark
|
||||||
HStack(spacing: 4) {
|
// would say it twice.
|
||||||
Spacer(minLength: 0)
|
Image(systemName: "doc.on.doc")
|
||||||
if showingCopied {
|
.font(.footnote.weight(.semibold))
|
||||||
Text("Copied")
|
.foregroundStyle(.secondary)
|
||||||
.foregroundStyle(.green)
|
|
||||||
.transition(.opacity)
|
|
||||||
}
|
|
||||||
Image(systemName: showingCopied ? "checkmark" : "doc.on.doc")
|
|
||||||
.foregroundStyle(showingCopied ? Color.green : Color.secondary)
|
|
||||||
}
|
|
||||||
.font(.footnote.weight(.semibold))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
@@ -170,6 +167,29 @@ struct StationDetailView: View {
|
|||||||
? ""
|
? ""
|
||||||
: String(localized: "Copies the postcode",
|
: String(localized: "Copies the postcode",
|
||||||
comment: "Accessibility hint for the address row in the More info sheet"))
|
comment: "Accessibility hint for the address row in the More info sheet"))
|
||||||
|
// Horizontally centred over the address line itself.
|
||||||
|
.overlay(alignment: .center) {
|
||||||
|
if showingCopied {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
Image(systemName: "checkmark")
|
||||||
|
Text("Copied")
|
||||||
|
}
|
||||||
|
.font(.footnote.weight(.semibold))
|
||||||
|
.foregroundStyle(.green)
|
||||||
|
.padding(.horizontal, 10)
|
||||||
|
.padding(.vertical, 6)
|
||||||
|
// A material base keeps it legible where the tint
|
||||||
|
// alone (the notice-capsule recipe) washes out.
|
||||||
|
.background {
|
||||||
|
Capsule()
|
||||||
|
.fill(.regularMaterial)
|
||||||
|
.overlay(Capsule().fill(Color.green.opacity(0.22)))
|
||||||
|
}
|
||||||
|
.allowsHitTesting(false)
|
||||||
|
.accessibilityHidden(true)
|
||||||
|
.transition(.opacity)
|
||||||
|
}
|
||||||
|
}
|
||||||
if let location {
|
if let location {
|
||||||
let distance = FuelStore.stationDistanceDisplay(
|
let distance = FuelStore.stationDistanceDisplay(
|
||||||
station: station, userLat: location.lat, userLng: location.lng
|
station: station, userLat: location.lat, userLng: location.lng
|
||||||
@@ -223,6 +243,13 @@ struct StationDetailView: View {
|
|||||||
} header: {
|
} header: {
|
||||||
Text("Best Value")
|
Text("Best Value")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The explanation lives in its own sheet now, opened from this
|
||||||
|
// row. The row sits INSIDE the `if let` so it carries the same
|
||||||
|
// gate as the card above: no card, no row.
|
||||||
|
Section {
|
||||||
|
HowBestValueRow { showBestValueInfo = true }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
@@ -248,6 +275,8 @@ struct StationDetailView: View {
|
|||||||
} message: {
|
} message: {
|
||||||
Text("Are you sure you want to report an error in fuel prices or forecourt details? This will open in your chosen browser.")
|
Text("Are you sure you want to report an error in fuel prices or forecourt details? This will open in your chosen browser.")
|
||||||
}
|
}
|
||||||
|
// Nested sheet: the More info sheet stays put behind the explanation.
|
||||||
|
.sheet(isPresented: $showBestValueInfo) { HowBestValueSheet() }
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .cancellationAction) {
|
ToolbarItem(placement: .cancellationAction) {
|
||||||
Button("Done") { dismiss() }
|
Button("Done") { dismiss() }
|
||||||
|
|||||||
@@ -35,6 +35,11 @@ struct StationsView: View {
|
|||||||
/// info button in the header — keeps the list focused on stations.
|
/// info button in the header — keeps the list focused on stations.
|
||||||
@State private var showKey = false
|
@State private var showKey = false
|
||||||
|
|
||||||
|
/// The "How Best Value works" sheet. The explanation used to be a section in
|
||||||
|
/// the Key sheet; it is its own sheet now, opened from a row at the bottom of
|
||||||
|
/// the Key sheet and from the More info sheet.
|
||||||
|
@State private var showBestValueInfo = false
|
||||||
|
|
||||||
/// Station picked for the More-info sheet (tap action = showDetails).
|
/// Station picked for the More-info sheet (tap action = showDetails).
|
||||||
@State private var detailStation: FuelStation?
|
@State private var detailStation: FuelStation?
|
||||||
|
|
||||||
@@ -218,6 +223,18 @@ struct StationsView: View {
|
|||||||
if ProcessInfo.processInfo.arguments.contains("-showKeySheet") {
|
if ProcessInfo.processInfo.arguments.contains("-showKeySheet") {
|
||||||
showKey = true
|
showKey = true
|
||||||
}
|
}
|
||||||
|
// QA hook: `-showBestValueInfo` opens the Key sheet and then the
|
||||||
|
// explanation on top of it, so the copy can be screenshotted
|
||||||
|
// without a tap. It has to chain: the explanation's `.sheet` lives
|
||||||
|
// inside the Key sheet's content, so it cannot present while the
|
||||||
|
// Key sheet is shut.
|
||||||
|
if BestValueInfoQA.isRequested {
|
||||||
|
showKey = true
|
||||||
|
Task { @MainActor in
|
||||||
|
try? await Task.sleep(nanoseconds: 1_200_000_000)
|
||||||
|
showBestValueInfo = true
|
||||||
|
}
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
.task(id: stations.count) {
|
.task(id: stations.count) {
|
||||||
@@ -299,15 +316,12 @@ struct StationsView: View {
|
|||||||
.listRowInsets(EdgeInsets(top: 4, leading: 16, bottom: 4, trailing: 16))
|
.listRowInsets(EdgeInsets(top: 4, leading: 16, bottom: 4, trailing: 16))
|
||||||
}
|
}
|
||||||
|
|
||||||
Section("How Best Value works") {
|
// The explanation is its own sheet now, opened from this
|
||||||
VStack(alignment: .leading, spacing: 10) {
|
// row. Deliberately NOT gated by Best Value: the Key sheet
|
||||||
Text("Best Value compares each station with the closest one. It works out what you could save at the pump, then takes off the fuel used for the extra return journey.")
|
// is the legend and is reachable whatever the feature is
|
||||||
Text("Your fill-up amount and MPG are used to show whether you could save overall.")
|
// doing.
|
||||||
Text("Results are marked “Approx.” until road distances are available.")
|
Section {
|
||||||
.foregroundStyle(.secondary)
|
HowBestValueRow { showBestValueInfo = true }
|
||||||
}
|
|
||||||
.font(.subheadline)
|
|
||||||
.padding(.vertical, 4)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle("Key")
|
.navigationTitle("Key")
|
||||||
@@ -317,6 +331,8 @@ struct StationsView: View {
|
|||||||
Button("Done") { showKey = false }
|
Button("Done") { showKey = false }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Nested sheet: the Key sheet stays behind the explanation.
|
||||||
|
.sheet(isPresented: $showBestValueInfo) { HowBestValueSheet() }
|
||||||
}
|
}
|
||||||
.presentationDetents([.fraction(0.6)])
|
.presentationDetents([.fraction(0.6)])
|
||||||
.presentationBackground(Color(UIColor.systemGroupedBackground))
|
.presentationBackground(Color(UIColor.systemGroupedBackground))
|
||||||
|
|||||||
Reference in New Issue
Block a user