import UserNotifications /// Rich (image) push — see docs/api/push-notifications-integration-guide.md §4.3. /// Only fires when Atomenta's push payload sets `"mutable-content": 1`, which /// it does by default on any push carrying an image (real wire key is /// `"image"`, not the guide's `"imageUrl"` — verified against /// `PushNotificationService.dispatchApns`/`dispatchApnsMany` in Atomenta). /// `@unchecked Sendable`: the extension process only ever runs one /// `didReceive`/`serviceExtensionTimeWillExpire` cycle at a time — there's no /// real concurrent access to `contentHandler`/`bestAttemptContent` to guard /// against, just a background download completion handing back to this /// instance. final class NotificationService: UNNotificationServiceExtension, @unchecked Sendable { private var contentHandler: ((UNNotificationContent) -> Void)? private var bestAttemptContent: UNMutableNotificationContent? override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { self.contentHandler = contentHandler let mutableContent = (request.content.mutableCopy() as? UNMutableNotificationContent) ?? UNMutableNotificationContent() bestAttemptContent = mutableContent guard let imageURLString = request.content.userInfo["image"] as? String, let imageURL = URL(string: imageURLString) else { deliver() return } URLSession.shared.downloadTask(with: imageURL) { [weak self] location, _, _ in if let location, let attachment = Self.attachment(fromDownloadedFile: location) { self?.bestAttemptContent?.attachments = [attachment] } self?.deliver() }.resume() } /// The system calls this if `didReceive` doesn't finish within its time /// budget — must still deliver the best content built so far, since /// `contentHandler` is contractually required to run exactly once. override func serviceExtensionTimeWillExpire() { deliver() } private func deliver() { guard let bestAttemptContent else { return } contentHandler?(bestAttemptContent) contentHandler = nil } private static func attachment(fromDownloadedFile location: URL) -> UNNotificationAttachment? { let tmpURL = FileManager.default.temporaryDirectory .appendingPathComponent(UUID().uuidString) .appendingPathExtension("jpg") do { try FileManager.default.moveItem(at: location, to: tmpURL) return try UNNotificationAttachment(identifier: "image", url: tmpURL) } catch { return nil } } }