From cbe01f4219f0e759a7363044ea59ae11ada2049c Mon Sep 17 00:00:00 2001 From: FuelBoard Contributor Date: Wed, 12 Aug 2026 10:21:59 +0100 Subject: [PATCH] Single notification delivery: map renders before add() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- FuelBoard/ProximityMonitor.swift | 43 +++++++++---------- .../Logs/Launch/LogStoreManifest.plist | 10 +++++ .../Logs/Localization/LogStoreManifest.plist | 10 +++++ .../Logs/Package/LogStoreManifest.plist | 10 +++++ build-tests/Logs/Test/LogStoreManifest.plist | 10 +++++ 5 files changed, 61 insertions(+), 22 deletions(-) create mode 100644 build-tests/Logs/Launch/LogStoreManifest.plist create mode 100644 build-tests/Logs/Localization/LogStoreManifest.plist create mode 100644 build-tests/Logs/Package/LogStoreManifest.plist create mode 100644 build-tests/Logs/Test/LogStoreManifest.plist diff --git a/FuelBoard/ProximityMonitor.swift b/FuelBoard/ProximityMonitor.swift index ee00a1d..5bbd82e 100644 --- a/FuelBoard/ProximityMonitor.swift +++ b/FuelBoard/ProximityMonitor.swift @@ -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) } } } diff --git a/build-tests/Logs/Launch/LogStoreManifest.plist b/build-tests/Logs/Launch/LogStoreManifest.plist new file mode 100644 index 0000000..236d5e2 --- /dev/null +++ b/build-tests/Logs/Launch/LogStoreManifest.plist @@ -0,0 +1,10 @@ + + + + + logFormatVersion + 12 + logs + + + diff --git a/build-tests/Logs/Localization/LogStoreManifest.plist b/build-tests/Logs/Localization/LogStoreManifest.plist new file mode 100644 index 0000000..236d5e2 --- /dev/null +++ b/build-tests/Logs/Localization/LogStoreManifest.plist @@ -0,0 +1,10 @@ + + + + + logFormatVersion + 12 + logs + + + diff --git a/build-tests/Logs/Package/LogStoreManifest.plist b/build-tests/Logs/Package/LogStoreManifest.plist new file mode 100644 index 0000000..236d5e2 --- /dev/null +++ b/build-tests/Logs/Package/LogStoreManifest.plist @@ -0,0 +1,10 @@ + + + + + logFormatVersion + 12 + logs + + + diff --git a/build-tests/Logs/Test/LogStoreManifest.plist b/build-tests/Logs/Test/LogStoreManifest.plist new file mode 100644 index 0000000..236d5e2 --- /dev/null +++ b/build-tests/Logs/Test/LogStoreManifest.plist @@ -0,0 +1,10 @@ + + + + + logFormatVersion + 12 + logs + + +