Single notification delivery: map renders before add()

LiveContainer did not honour the replace-in-place contract: adding a
second request with the same identifier delivered a SECOND notification
instead of updating the first. Now the MKMapSnapshotter renders first
(10s timeout), then exactly ONE request is added with the map attached.
If the snapshot fails or times out, the notification still fires without
the image — never two notifications.
This commit is contained in:
FuelBoard Contributor
2026-08-12 10:21:59 +01:00
parent b56d2a129c
commit cbe01f4219
5 changed files with 61 additions and 22 deletions
+21 -22
View File
@@ -209,13 +209,13 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca
]
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.
// Render the map FIRST, then deliver ONE notification with the map
// attached. (Earlier, delivering immediately and "replacing" via a
// second add() showed up as two notifications in LiveContainer the
// replace-in-place contract isn't honoured there.) A timeout
// guarantees the alert still fires, without the map, if the snapshot
// hangs.
mapStatus?("map: preparing…")
UNUserNotificationCenter.current().add(
UNNotificationRequest(identifier: identifier, content: content, trigger: nil)
)
let options = MKMapSnapshotter.Options()
options.region = MKCoordinateRegion(
@@ -226,24 +226,28 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca
options.mapType = .standard
let snapshotter = MKMapSnapshotter(options: options)
// Timeout so a hung snapshot can't leave the result line stuck on
// "preparing" forever.
var finished = false
let deliver: (String) -> Void = { status in
guard !finished else { return }
finished = true
mapStatus?(status)
UNUserNotificationCenter.current().add(
UNNotificationRequest(identifier: identifier, content: content, trigger: nil)
)
}
// Timeout so a hung snapshot can't leave the alert undelivered (and
// the result line stuck on "preparing") forever.
let timeout = DispatchWorkItem {
Task { @MainActor in
guard !finished else { return }
finished = true
mapStatus?("map: timed out")
deliver("map: timed out — no image")
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 8, execute: timeout)
DispatchQueue.main.asyncAfter(deadline: .now() + 10, execute: timeout)
snapshotter.start { [content] snapshot, error in
timeout.cancel()
Task { @MainActor in
guard !finished else { return }
finished = true
let finalContent = content
var status = "map: no image"
if let snapshot, error == nil,
let image = Self.mapImage(snapshot: snapshot, station: station) {
@@ -251,18 +255,13 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca
.appendingPathComponent("alert-map-\(UUID().uuidString).png")
if let data = image.pngData(), (try? data.write(to: url)) != nil,
let attachment = try? UNNotificationAttachment(identifier: "map", url: url, options: nil) {
finalContent.attachments = [attachment]
content.attachments = [attachment]
status = "map: attached"
}
} else if let error {
status = "map: snapshot failed (\(error.localizedDescription))"
}
mapStatus?(status)
// Replace the delivered notification (same identifier) so the
// map appears in place.
UNUserNotificationCenter.current().add(
UNNotificationRequest(identifier: identifier, content: finalContent, trigger: nil)
)
deliver(status)
}
}
}