How Best Value works: a Setup Best Value button, and a clean hand-off to Settings
The explanation sheet now carries a full-width button that closes it and the sheet it was opened from, then lands on Settings — whose Best Value section is the first thing in its list, so the controls are on screen on arrival. It uses .controlSize(.large), the standard iOS button height. Both the More info sheet and the Key sheet are presented by StationsView, and FavouritesView presents its own More info sheet — not ContentView's monitor as assumed — so each host closes whichever sheet it owns on the notification. Before this, one of them stayed up over the Settings tab. The button now announces itself half a second after its own dismissal, so the child and parent dismissals cannot race and drop one of the two.
This commit is contained in:
@@ -865,6 +865,15 @@ struct ContentView: View {
|
|||||||
.onReceive(NotificationCenter.default.publisher(for: .fuelBoardRoadDistanceDebugUpdated)) { _ in
|
.onReceive(NotificationCenter.default.publisher(for: .fuelBoardRoadDistanceDebugUpdated)) { _ in
|
||||||
roadDistanceDebugStatus = RoadDistanceService.currentDebugStatus()
|
roadDistanceDebugStatus = RoadDistanceService.currentDebugStatus()
|
||||||
}
|
}
|
||||||
|
.onReceive(NotificationCenter.default.publisher(for: .fuelBoardOpenBestValueSettings)) { _ in
|
||||||
|
// "Setup Best Value" in the How Best Value works sheet: close
|
||||||
|
// the More info sheet it was opened from (nil dismisses it —
|
||||||
|
// the sheet is item-driven) and switch to Settings. Tag 3 is
|
||||||
|
// the Settings tab; its Best Value section is the first item
|
||||||
|
// in the list, so the controls are on screen on arrival.
|
||||||
|
monitor.pendingDetailStation = nil
|
||||||
|
selectedTab = 3
|
||||||
|
}
|
||||||
.onReceive(NotificationCenter.default.publisher(for: .fuelBoardShowStationDetail)) { note in
|
.onReceive(NotificationCenter.default.publisher(for: .fuelBoardShowStationDetail)) { note in
|
||||||
let info = note.userInfo ?? [:]
|
let info = note.userInfo ?? [:]
|
||||||
if let id = info["stationID"] as? String,
|
if let id = info["stationID"] as? String,
|
||||||
|
|||||||
@@ -222,6 +222,12 @@ struct FavouritesView: View {
|
|||||||
onToggleFavourite: { onToggleFavourite(station, activeFuel) }
|
onToggleFavourite: { onToggleFavourite(station, activeFuel) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
.onReceive(NotificationCenter.default.publisher(for: .fuelBoardOpenBestValueSettings)) { _ in
|
||||||
|
// "Setup Best Value" from the explanation sheet, opened inside
|
||||||
|
// this view's More info sheet. That sheet closes itself; this one
|
||||||
|
// is presented from HERE and would otherwise stay over Settings.
|
||||||
|
detailStation = nil
|
||||||
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// QA hook: launch with `-showTrends` to open the sheet
|
// QA hook: launch with `-showTrends` to open the sheet
|
||||||
|
|||||||
@@ -16,6 +16,22 @@ struct HowBestValueSheet: View {
|
|||||||
VStack(alignment: .leading, spacing: 10) {
|
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("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.")
|
Text("Your fill-up amount and MPG are used to show whether you could save overall.")
|
||||||
|
// The whole point of the sheet is to explain the feature,
|
||||||
|
// so the way to switch it on lives here rather than only
|
||||||
|
// in Settings. Full-width: this is the card's one action.
|
||||||
|
Button {
|
||||||
|
openBestValueSettings()
|
||||||
|
} label: {
|
||||||
|
Text("Setup Best Value")
|
||||||
|
.font(.subheadline.weight(.semibold))
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
// `.large` is the standard iOS button height (~50pt): the
|
||||||
|
// default `.borderedProminent` is considerably shorter and
|
||||||
|
// read as a compact control rather than a button.
|
||||||
|
.buttonStyle(.borderedProminent)
|
||||||
|
.controlSize(.large)
|
||||||
|
.padding(.top, 2)
|
||||||
}
|
}
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.padding(.vertical, 4)
|
.padding(.vertical, 4)
|
||||||
@@ -63,6 +79,33 @@ struct HowBestValueSheet: View {
|
|||||||
}
|
}
|
||||||
.presentationDetents([.fraction(0.72)])
|
.presentationDetents([.fraction(0.72)])
|
||||||
.presentationBackground(Color(UIColor.systemGroupedBackground))
|
.presentationBackground(Color(UIColor.systemGroupedBackground))
|
||||||
|
#if DEBUG
|
||||||
|
// QA hook: `-qaSetupBestValue` runs the Setup Best Value action without a
|
||||||
|
// tap (simctl cannot tap), so the tab switch can be screenshotted.
|
||||||
|
.task {
|
||||||
|
guard ProcessInfo.processInfo.arguments.contains("-qaSetupBestValue") else { return }
|
||||||
|
try? await Task.sleep(nanoseconds: 2_000_000_000)
|
||||||
|
openBestValueSettings()
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Closes this sheet and asks the app to land on Settings → Best Value, whose
|
||||||
|
/// section is the first thing in its list and holds the fill-up amount, MPG
|
||||||
|
/// and minimum saving. This sheet sits two levels below the tab view — More
|
||||||
|
/// info sheet, or the Key sheet — so it cannot switch tabs itself: it closes
|
||||||
|
/// itself and posts, and ContentView does the rest.
|
||||||
|
private func openBestValueSettings() {
|
||||||
|
dismiss()
|
||||||
|
// Announce only once this sheet has animated away. The host then closes
|
||||||
|
// the sheet this one was presented from — the More info sheet, or the Key
|
||||||
|
// sheet — and switches to Settings. Asking for that while the child was
|
||||||
|
// still dismissing could drop one of the two dismissals, which is what
|
||||||
|
// left the Settings tab covered by a sheet.
|
||||||
|
Task { @MainActor in
|
||||||
|
try? await Task.sleep(nanoseconds: 500_000_000)
|
||||||
|
NotificationCenter.default.post(name: .fuelBoardOpenBestValueSettings, object: nil)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One numbered step: a lead-in, then the plain explanation. Same words
|
/// One numbered step: a lead-in, then the plain explanation. Same words
|
||||||
|
|||||||
@@ -335,6 +335,14 @@ struct StationDetailView: View {
|
|||||||
try? await Task.sleep(nanoseconds: 3_000_000_000)
|
try? await Task.sleep(nanoseconds: 3_000_000_000)
|
||||||
withAnimation { proxy.scrollTo("bestValueCard", anchor: .bottom) }
|
withAnimation { proxy.scrollTo("bestValueCard", anchor: .bottom) }
|
||||||
}
|
}
|
||||||
|
.task {
|
||||||
|
// QA hook: `-qaDetailShowBestValueInfo` opens the explanation from
|
||||||
|
// THIS sheet (the More info path), so the two-sheet dismissal can
|
||||||
|
// be screenshotted — simctl has no tap.
|
||||||
|
guard ProcessInfo.processInfo.arguments.contains("-qaDetailShowBestValueInfo") else { return }
|
||||||
|
try? await Task.sleep(nanoseconds: 1_500_000_000)
|
||||||
|
showBestValueInfo = true
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
.alert("Report an error?", isPresented: $showingReportIssueConfirmation) {
|
.alert("Report an error?", isPresented: $showingReportIssueConfirmation) {
|
||||||
Button("Cancel", role: .cancel) { }
|
Button("Cancel", role: .cancel) { }
|
||||||
|
|||||||
@@ -218,6 +218,15 @@ struct StationsView: View {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
.onReceive(NotificationCenter.default.publisher(for: .fuelBoardOpenBestValueSettings)) { _ in
|
||||||
|
// "Setup Best Value" from the explanation sheet. That sheet closes
|
||||||
|
// itself and ContentView switches to Settings, but both of the
|
||||||
|
// sheets this view can have open are presented from HERE and each
|
||||||
|
// survived the tab switch, floating over Settings: the Key sheet
|
||||||
|
// and the More info sheet. Close whichever is up.
|
||||||
|
showKey = false
|
||||||
|
detailStation = nil
|
||||||
|
}
|
||||||
.task(id: stations.count) {
|
.task(id: stations.count) {
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
// QA hook: `-showDetail` opens the More info sheet for the first
|
// QA hook: `-showDetail` opens the More info sheet for the first
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ import WatchConnectivity
|
|||||||
extension Notification.Name {
|
extension Notification.Name {
|
||||||
static let fuelBoardWatchRefreshRequested = Notification.Name("FuelBoardWatchRefreshRequested")
|
static let fuelBoardWatchRefreshRequested = Notification.Name("FuelBoardWatchRefreshRequested")
|
||||||
static let fuelBoardShowStationDetail = Notification.Name("FuelBoardShowStationDetail")
|
static let fuelBoardShowStationDetail = Notification.Name("FuelBoardShowStationDetail")
|
||||||
|
/// Posted by the "How Best Value works" sheet's Setup Best Value button, so
|
||||||
|
/// the app can close that sheet's stack and land on Settings — whose Best
|
||||||
|
/// Value section is the first thing in its list. The sheet cannot switch
|
||||||
|
/// tabs itself: it is presented two levels below the tab view.
|
||||||
|
static let fuelBoardOpenBestValueSettings = Notification.Name("FuelBoardOpenBestValueSettings")
|
||||||
/// Posted when a road-distance fill finishes, so the app's rows re-read the
|
/// Posted when a road-distance fill finishes, so the app's rows re-read the
|
||||||
/// distance they are showing — those reads happen inside the row views, so
|
/// distance they are showing — those reads happen inside the row views, so
|
||||||
/// nothing else would invalidate them.
|
/// nothing else would invalidate them.
|
||||||
|
|||||||
Reference in New Issue
Block a user