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.
148 lines
5.9 KiB
Swift
148 lines
5.9 KiB
Swift
import Foundation
|
|
import WatchConnectivity
|
|
|
|
extension Notification.Name {
|
|
static let fuelBoardWatchRefreshRequested = Notification.Name("FuelBoardWatchRefreshRequested")
|
|
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
|
|
/// distance they are showing — those reads happen inside the row views, so
|
|
/// nothing else would invalidate them.
|
|
static let fuelBoardRoadDistancesUpdated = Notification.Name("FuelBoardRoadDistancesUpdated")
|
|
/// Posted when "Accurate distances" is switched, so the browse layer can
|
|
/// re-render and start a fill from where the user actually is.
|
|
static let fuelBoardAccurateDistancesChanged = Notification.Name("FuelBoardAccurateDistancesChanged")
|
|
/// Posted whenever the road-distance fill starts, finishes, or is gated.
|
|
/// Settings → Debug subscribes so the tester can see why Approx. remains.
|
|
static let fuelBoardRoadDistanceDebugUpdated = Notification.Name("FuelBoardRoadDistanceDebugUpdated")
|
|
}
|
|
|
|
@MainActor
|
|
final class WatchSyncManager: NSObject, WCSessionDelegate {
|
|
static let shared = WatchSyncManager()
|
|
|
|
private var pendingSnapshotPush = false
|
|
private var activationRequested = false
|
|
|
|
private override init() {
|
|
super.init()
|
|
}
|
|
|
|
func activate() {
|
|
guard WCSession.isSupported() else { return }
|
|
let session = WCSession.default
|
|
guard session.isWatchAppInstalled else { return }
|
|
pendingSnapshotPush = true
|
|
if session.delegate !== self {
|
|
session.delegate = self
|
|
}
|
|
guard session.activationState != .activated else { return }
|
|
guard !activationRequested else { return }
|
|
activationRequested = true
|
|
session.activate()
|
|
}
|
|
|
|
func pushSnapshot() {
|
|
guard WCSession.isSupported() else { return }
|
|
let session = WCSession.default
|
|
guard session.isWatchAppInstalled else {
|
|
pendingSnapshotPush = false
|
|
return
|
|
}
|
|
guard session.activationState == .activated else {
|
|
pendingSnapshotPush = true
|
|
activate()
|
|
return
|
|
}
|
|
do {
|
|
try session.updateApplicationContext(snapshotContext())
|
|
pendingSnapshotPush = false
|
|
} catch {
|
|
print("WATCH-SYNC push failed: \(error.localizedDescription)")
|
|
}
|
|
}
|
|
|
|
private func snapshotContext() -> [String: Any] {
|
|
var context: [String: Any] = [:]
|
|
|
|
if let favourites = try? JSONEncoder().encode(FuelStore.loadFavourites()) {
|
|
context[FuelStore.favouritesKey] = favourites
|
|
}
|
|
// Do NOT send the full station dump to watchOS via WatchConnectivity.
|
|
// The UK dataset is megabytes large and exceeds application-context
|
|
// payload limits, which prevents any snapshot from arriving. The
|
|
// favourites payload already carries station snapshots + prices, so the
|
|
// watch can render favourites without the full list.
|
|
|
|
context[FuelStore.fuelKey] = FuelStore.loadSelectedFuel().rawValue
|
|
context[FuelStore.distanceUnitKey] = FuelStore.loadDistanceUnit().rawValue
|
|
context[FuelStore.priceDisplayStyleKey] = FuelStore.loadPriceDisplayStyle().rawValue
|
|
|
|
if let lastRefresh = FuelStore.loadLastRefresh() {
|
|
context[FuelStore.lastRefreshKey] = String(lastRefresh.timeIntervalSince1970)
|
|
}
|
|
|
|
if let handled = FuelStore.loadWatchRefreshHandled() {
|
|
context[FuelStore.watchRefreshHandledKey] = String(handled.timeIntervalSince1970)
|
|
}
|
|
|
|
if let location = FuelStore.loadLocationWithDate() {
|
|
context[FuelStore.locationKey] = "\(location.coordinate.lat),\(location.coordinate.lng),\(location.date.timeIntervalSince1970)"
|
|
}
|
|
|
|
return context
|
|
}
|
|
|
|
nonisolated func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: (any Error)?) {
|
|
Task { @MainActor in
|
|
self.activationRequested = false
|
|
}
|
|
if let error {
|
|
print("WATCH-SYNC activation failed: \(error.localizedDescription)")
|
|
return
|
|
}
|
|
Task { @MainActor in
|
|
if activationState == .activated, self.pendingSnapshotPush {
|
|
self.pushSnapshot()
|
|
}
|
|
}
|
|
}
|
|
|
|
nonisolated func sessionDidBecomeInactive(_ session: WCSession) {}
|
|
|
|
nonisolated func sessionDidDeactivate(_ session: WCSession) {
|
|
Task { @MainActor in
|
|
self.activationRequested = false
|
|
self.activate()
|
|
}
|
|
}
|
|
|
|
nonisolated func session(_ session: WCSession, didReceiveUserInfo userInfo: [String : Any] = [:]) {
|
|
handleIncomingMessage(userInfo)
|
|
}
|
|
|
|
nonisolated func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
|
|
handleIncomingMessage(message)
|
|
}
|
|
|
|
private nonisolated func handleIncomingMessage(_ payload: [String: Any]) {
|
|
if payload["fuelboard.snapshotRequest"] as? String != nil {
|
|
Task { @MainActor in
|
|
self.pushSnapshot()
|
|
}
|
|
}
|
|
|
|
guard let raw = payload[FuelStore.watchRefreshRequestKey] as? String,
|
|
let timestamp = TimeInterval(raw) else { return }
|
|
let date = Date(timeIntervalSince1970: timestamp)
|
|
Task { @MainActor in
|
|
FuelStore.requestWatchRefresh(date)
|
|
NotificationCenter.default.post(name: .fuelBoardWatchRefreshRequested, object: nil)
|
|
}
|
|
}
|
|
}
|