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 let identifier = UUID().uuidString
// Deliver the notification IMMEDIATELY the alert must never wait on // Render the map FIRST, then deliver ONE notification with the map
// the map. If the snapshot later succeeds, the same identifier is used // attached. (Earlier, delivering immediately and "replacing" via a
// to replace the delivered notification with the map attached. // 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…") 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(
@@ -226,24 +226,28 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca
options.mapType = .standard options.mapType = .standard
let snapshotter = MKMapSnapshotter(options: options) let snapshotter = MKMapSnapshotter(options: options)
// Timeout so a hung snapshot can't leave the result line stuck on
// "preparing" forever.
var finished = false 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 { let timeout = DispatchWorkItem {
Task { @MainActor in Task { @MainActor in
guard !finished else { return } deliver("map: timed out — no image")
finished = true
mapStatus?("map: timed out")
} }
} }
DispatchQueue.main.asyncAfter(deadline: .now() + 8, execute: timeout) DispatchQueue.main.asyncAfter(deadline: .now() + 10, execute: timeout)
snapshotter.start { [content] snapshot, error in snapshotter.start { [content] snapshot, error in
timeout.cancel() timeout.cancel()
Task { @MainActor in Task { @MainActor in
guard !finished else { return }
finished = true
let finalContent = content
var status = "map: no image" var status = "map: no image"
if let snapshot, error == nil, if let snapshot, error == nil,
let image = Self.mapImage(snapshot: snapshot, station: station) { 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") .appendingPathComponent("alert-map-\(UUID().uuidString).png")
if let data = image.pngData(), (try? data.write(to: url)) != nil, if let data = image.pngData(), (try? data.write(to: url)) != nil,
let attachment = try? UNNotificationAttachment(identifier: "map", url: url, options: nil) { let attachment = try? UNNotificationAttachment(identifier: "map", url: url, options: nil) {
finalContent.attachments = [attachment] content.attachments = [attachment]
status = "map: attached" status = "map: attached"
} }
} else if let error { } else if let error {
status = "map: snapshot failed (\(error.localizedDescription))" status = "map: snapshot failed (\(error.localizedDescription))"
} }
mapStatus?(status) deliver(status)
// Replace the delivered notification (same identifier) so the
// map appears in place.
UNUserNotificationCenter.current().add(
UNNotificationRequest(identifier: identifier, content: finalContent, trigger: nil)
)
} }
} }
} }
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>logFormatVersion</key>
<integer>12</integer>
<key>logs</key>
<dict/>
</dict>
</plist>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>logFormatVersion</key>
<integer>12</integer>
<key>logs</key>
<dict/>
</dict>
</plist>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>logFormatVersion</key>
<integer>12</integer>
<key>logs</key>
<dict/>
</dict>
</plist>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>logFormatVersion</key>
<integer>12</integer>
<key>logs</key>
<dict/>
</dict>
</plist>