Rewrite `UNNotificationServiceExtension` sub class into Swift 6 async await notation

I'm trying to rewrite a Swift code to Swift 6 language mode and am stuck with this problem. How do I safely pass the bestAttemptContent and contentHandler to the Task? This is from the UNNotificationServiceExtension subclass.

    final class NotificationService: UNNotificationServiceExtension {
    
      var contentHandler: ((UNNotificationContent) -> Void)?
      var bestAttemptContent: UNMutableNotificationContent?
      var customNotificationTask: Task<Void, Error>?
    
      override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
        self.contentHandler = contentHandler
        bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
        guard let bestAttemptContent = bestAttemptContent else {
    
          invokeContentHandler(with: request.content)
          return
        }
    
        do {
          let notificationModel = try PushNotificationUserInfo(data: request.content.userInfo)
          guard let templatedImageUrl = notificationModel.templatedImageUrlString,
                let imageUrl = imageUrl(from: templatedImageUrl) else {
            invokeContentHandler(with: bestAttemptContent)
            return
          }
          setupCustomNotificationTask(
            imageUrl: imageUrl,
            bestAttemptContent: bestAttemptContent,
            contentHandler: contentHandler
          )
        } catch {
          invokeContentHandler(with: bestAttemptContent)
        }
      }
    
      // More code
    
      private func downloadImageTask(
            imageUrl: URL,
            bestAttemptContent: UNMutableNotificationContent,
            contentHandler: @escaping (UNNotificationContent) -> Void
      ) {
            self.customNotificationTask = Task {
              let (location, _) = try await URLSession.shared.download(from: imageUrl)
              let desiredLocation = URL(fileURLWithPath: "\(location.path)\(imageUrl.lastPathComponent)")
              
                try FileManager.default.moveItem(at: location, to: desiredLocation)
                let attachment = try UNNotificationAttachment(identifier: imageUrl.absoluteString, url: desiredLocation, options: nil)
              bestAttemptContent.attachments = [attachment]
              contentHandler(bestAttemptContent)
            }
       }
    }
  • I tried using the MainActor.run {}, but it just moved the error to that run function.
  • The UNNotificationRequest is not sendable, and I don't think I can make it so.
  • Wrap the setupCustomNotification in a Task will move the errors to the didReceive method.
  • It seems like the consuming keyword will help here, but it leads to a compilation error, even with the latest Xcode (16.2).

Any pointers?

It’s hard to answer this without expertise in the UNNotificationServiceExtension type itself, something that I don’t have. However, let me at least set the stage…

Presumably your contentHandler type is meant to match the contentHandler parameter of didReceive(_:withContentHandler:). Is it legal for your NSE to call that from a secondary thread? Or is the system expecting you to call that on the main thread?

Once you know the answer then that’ll guide your use of that type:

  • If the system allows it to be called from any thread, it should be sendable.

  • If not, it should be main actor bound.

And once you know that you can adjust the contentHandler parameter of downloadImageTask(…) accordingly.

If you don’t know the answer to this question already, I recommend that you ask over on Apple Developer Forums, and specifically in the App & System Services > Notifications topic area.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

I updated the question.

I think the MainActor argument is not the solution here because I tried to move this whole function inside a Task { @MainActor in } annotation, and it complains the same things as both captured variables are not Sendable.