Deliver alert immediately; map attaches in place afterwards
The notification used to be added only inside the MKMapSnapshotter completion — if the snapshot hung or failed, no notification was ever delivered and the result line never showed the map status. Now the notification fires immediately (same identifier), the snapshot renders in the background with an 8s timeout, and on success the delivered notification is replaced in place with the map attached. Result line always updates: preparing -> attached / failed / timed out.
This commit is contained in:
@@ -207,6 +207,16 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca
|
|||||||
"stationLat": station.lat,
|
"stationLat": station.lat,
|
||||||
"stationLng": station.lng,
|
"stationLng": station.lng,
|
||||||
]
|
]
|
||||||
|
let identifier = UUID().uuidString
|
||||||
|
|
||||||
|
// Deliver the notification IMMEDIATELY — the alert must never wait on
|
||||||
|
// the map. If the snapshot later succeeds, the same identifier is used
|
||||||
|
// to replace the delivered notification with the map attached.
|
||||||
|
mapStatus?("map: preparing…")
|
||||||
|
UNUserNotificationCenter.current().add(
|
||||||
|
UNNotificationRequest(identifier: identifier, content: content, trigger: nil)
|
||||||
|
)
|
||||||
|
|
||||||
let options = MKMapSnapshotter.Options()
|
let options = MKMapSnapshotter.Options()
|
||||||
options.region = MKCoordinateRegion(
|
options.region = MKCoordinateRegion(
|
||||||
center: CLLocationCoordinate2D(latitude: station.lat, longitude: station.lng),
|
center: CLLocationCoordinate2D(latitude: station.lat, longitude: station.lng),
|
||||||
@@ -215,8 +225,24 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca
|
|||||||
options.size = CGSize(width: 800, height: 400)
|
options.size = CGSize(width: 800, height: 400)
|
||||||
options.mapType = .standard
|
options.mapType = .standard
|
||||||
let snapshotter = MKMapSnapshotter(options: options)
|
let snapshotter = MKMapSnapshotter(options: options)
|
||||||
snapshotter.start { [content] snapshot, error in
|
|
||||||
|
// Timeout so a hung snapshot can't leave the result line stuck on
|
||||||
|
// "preparing" forever.
|
||||||
|
var finished = false
|
||||||
|
let timeout = DispatchWorkItem {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
|
guard !finished else { return }
|
||||||
|
finished = true
|
||||||
|
mapStatus?("map: timed out")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 8, execute: timeout)
|
||||||
|
|
||||||
|
snapshotter.start { [content] snapshot, error in
|
||||||
|
timeout.cancel()
|
||||||
|
Task { @MainActor in
|
||||||
|
guard !finished else { return }
|
||||||
|
finished = true
|
||||||
let finalContent = content
|
let finalContent = content
|
||||||
var status = "map: no image"
|
var status = "map: no image"
|
||||||
if let snapshot, error == nil,
|
if let snapshot, error == nil,
|
||||||
@@ -232,8 +258,11 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca
|
|||||||
status = "map: snapshot failed (\(error.localizedDescription))"
|
status = "map: snapshot failed (\(error.localizedDescription))"
|
||||||
}
|
}
|
||||||
mapStatus?(status)
|
mapStatus?(status)
|
||||||
let request = UNNotificationRequest(identifier: UUID().uuidString, content: finalContent, trigger: nil)
|
// Replace the delivered notification (same identifier) so the
|
||||||
UNUserNotificationCenter.current().add(request)
|
// map appears in place.
|
||||||
|
UNUserNotificationCenter.current().add(
|
||||||
|
UNNotificationRequest(identifier: identifier, content: finalContent, trigger: nil)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -282,10 +311,10 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca
|
|||||||
content.body = "\(candidate.name) · \(distanceText) away · \(String(format: "%.1fp", price)). Tap for directions."
|
content.body = "\(candidate.name) · \(distanceText) away · \(String(format: "%.1fp", price)). Tap for directions."
|
||||||
content.sound = .default
|
content.sound = .default
|
||||||
let baseResult = "\(fuel.displayName) · \(brand) · \(String(format: "%.1fp", price)) · \(distanceText) away · radius \(unit.format(radiusKM))"
|
let baseResult = "\(fuel.displayName) · \(brand) · \(String(format: "%.1fp", price)) · \(distanceText) away · radius \(unit.format(radiusKM))"
|
||||||
|
lastTestResult = baseResult
|
||||||
addAlertRequest(content: content, station: candidate) { status in
|
addAlertRequest(content: content, station: candidate) { status in
|
||||||
self.lastTestResult = "\(baseResult) · \(status)"
|
self.lastTestResult = "\(baseResult) · \(status)"
|
||||||
}
|
}
|
||||||
lastTestResult = baseResult
|
|
||||||
} else {
|
} else {
|
||||||
// No real candidate (no stations loaded yet, or none sell the
|
// No real candidate (no stations loaded yet, or none sell the
|
||||||
// monitored fuel) — still fire so delivery is testable, but say
|
// monitored fuel) — still fire so delivery is testable, but say
|
||||||
|
|||||||
Reference in New Issue
Block a user