diff --git a/FuelBoard/ProximityMonitor.swift b/FuelBoard/ProximityMonitor.swift index 1238a10..ee00a1d 100644 --- a/FuelBoard/ProximityMonitor.swift +++ b/FuelBoard/ProximityMonitor.swift @@ -207,6 +207,16 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca "stationLat": station.lat, "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() options.region = MKCoordinateRegion( 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.mapType = .standard 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 + 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 var status = "map: no image" if let snapshot, error == nil, @@ -232,8 +258,11 @@ final class ProximityMonitor: NSObject, ObservableObject, @preconcurrency CLLoca status = "map: snapshot failed (\(error.localizedDescription))" } mapStatus?(status) - let request = UNNotificationRequest(identifier: UUID().uuidString, content: finalContent, trigger: nil) - UNUserNotificationCenter.current().add(request) + // Replace the delivered notification (same identifier) so the + // 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.sound = .default let baseResult = "\(fuel.displayName) · \(brand) · \(String(format: "%.1fp", price)) · \(distanceText) away · radius \(unit.format(radiusKM))" + lastTestResult = baseResult addAlertRequest(content: content, station: candidate) { status in self.lastTestResult = "\(baseResult) · \(status)" } - lastTestResult = baseResult } else { // No real candidate (no stations loaded yet, or none sell the // monitored fuel) — still fire so delivery is testable, but say