Request authorization for the notification center crash iOS app on Swift 6

Hey all!

During the migration of a production app to swift 6, I've encountered a problem: when hitting the UNUserNotificationCenter.current().requestAuthorization the app crashes.

If I switch back to Language Version 5 the app works as expected.

The offending code is defined here

class AppDelegate: NSObject, UIApplicationDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        FirebaseApp.configure()
        FirebaseConfiguration.shared.setLoggerLevel(.min)

        UNUserNotificationCenter.current().delegate = self

        let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
        UNUserNotificationCenter.current().requestAuthorization(options: authOptions) { _, _ in }

        application.registerForRemoteNotifications()

        Messaging.messaging().delegate = self

        return true
    }
}

The error is depicted here: I have no idea how to fix this.

Any help will be really appreciated

thanks in advance

Answered by DTS Engineer in 807248022
Written by DTS Engineer in 806202022
I’m gonna do some more research about this (FB15294185)

I spent some time talking this over with various colleagues, just to make sure I fully understand what’s going on. The high-level summary is:

  • Swift 6 has inserted a run-time check to catch a concurrency issue that Swift 5 did not.

  • This isn’t caught at compile time because of a Swift / Objective-C impedance mismatch.

If you want to know more, follow me down the rabbit hole!


Consider this simplified version of Artecoop’s test code:

@main
final class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    
    func applicationDidFinishLaunching(_ application: UIApplication) {
        let center = UNUserNotificationCenter.current()
        center.requestAuthorization(options: [.alert, .sound, .badge]) { success, error in
            print(success)
            assert(Thread.isMainThread)
        }
    }
}

The entire did-finish-launching method should run on the main actor. That’s because:

  • The method itself is part of the implementation of the UIApplicationDelegate protocol, and that’s declared @MainActor.

  • The requestAuthorization(…) method doesn’t do anything to tell the Swift compiler that it will call the completion handler on a secondary thread (more on that below).

If you build this program in Swift 6 mode, on running it you trap before the print(…) call, in a main actor check inserted by the Swift compiler. That’s the central complaint of the original post.

However, consider what happens if you build this in Swift 5 mode. This time it traps in the assert(…). So this trap is clearly an improvement: Swift 6 mode is consistently detecting a problem that could otherwise cause a data race.


However, that explanation leaves a number of unanswered questions. Let’s start with fixes.

The fix I described above, calling the async version of the method, works because the Swift compiler implements the async method with code that kinda looks something like this:

extension UNUserNotificationCenter {

    func requestNotificationQQQ1(
        options: UNAuthorizationOptions
    ) async throws -> Bool {
        try await withCheckedThrowingContinuation { cont in
            self.requestAuthorization(options: options) { success, error in
                if success {
                    cont.resume(returning: true)
                } else {
                    cont.resume(throwing: error!)
                }
            }
        }
    }
}

In this case the completion handler isn’t bound to the main actor and thus the compiler doesn’t add an assert. And the CheckedContinuation type is thread safe, so calling its methods from any thread is fine.

The other fix is to make the closure as @Sendable:

center.requestAuthorization(options: [.alert, .sound, .badge]) { @Sendable success, error in
    print(success)
    // print(self.window)
}

This disconnects the closure from the main actor and thus there’s no main actor check. However, it also means that you can’t access main actor state in the closure. If you uncomment the access to self.window, the compiler complains that Main actor-isolated property 'window' can not be referenced from a Sendable closure.


Finally, let’s come back to why the compiler doesn’t know that the completion handler can be called on any thread. That’s tied to the Objective-C declaration, which is imported into Swift as:

func requestAuthorization(
    options: UNAuthorizationOptions = [],
    completionHandler: @escaping (Bool, (any Error)?) -> Void
)

Imagine you wrote your own (very bad :-) [1] version of this in Swift:

extension UNUserNotificationCenter {

    func requestAuthorizationQQQ2(
        options: UNAuthorizationOptions = [],
        completionHandler: @escaping (Bool, (any Error)?) -> Void
    ) {
        DispatchQueue.global().asyncAfter(deadline: .now() + 0.1) {
            completionHandler(true, nil)
         // ^ Capture of 'completionHandler' with non-sendable type '(Bool, (any Error)?) -> Void' in a `@Sendable` closure
        }
    }
}

The compiler complains that the closure is being sent across isolation domains even though it’s not sendable. That’s obviously bad.

The thing to note here is that this is exactly what Objective-C is doing, and it’s why you’re running into the problem.

The most straightforward way to fix the requestAuthorizationQQQ2(…) method is to replace @escaping with @Sendable. And the equivalent of doing that in Objective-C is to add NS_SWIFT_SENDABLE to the completion handler parameter of the -requestAuthorizationWithOptions:completionHandler: method [2].

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] To start, I’m not following my own advice here: Avoid Dispatch Global Concurrent Queues

[2] It wouldn’t surprise me if that were the final resolution of FB15294185, but that’s still in The Future™.

Thanks for re-guiding me to your post. Indeed @Sendable solved the problem with the speech recognition permission.

Now another problem probably related to Apple Watch WCSession has occurred. The crash stack is nearly the same. However, I was not able to further isolate the problem. Here is a crash stack:

libdispatch.dylib`_dispatch_assert_queue_fail:

0x102588230 <+0>:   pacibsp 
0x102588234 <+4>:   sub    sp, sp, #0x50
0x102588238 <+8>:   stp    x20, x19, [sp, #0x30]
0x10258823c <+12>:  stp    x29, x30, [sp, #0x40]
0x102588240 <+16>:  add    x29, sp, #0x40
0x102588244 <+20>:  adrp   x8, 63
0x102588248 <+24>:  add    x8, x8, #0xe83            ; "not "
0x10258824c <+28>:  adrp   x9, 62
0x102588250 <+32>:  add    x9, x9, #0x6b6            ; ""
0x102588254 <+36>:  stur   xzr, [x29, #-0x18]
0x102588258 <+40>:  cmp    w1, #0x0
0x10258825c <+44>:  csel   x8, x9, x8, ne
0x102588260 <+48>:  ldr    x10, [x0, #0x48]
0x102588264 <+52>:  cmp    x10, #0x0
0x102588268 <+56>:  csel   x9, x9, x10, eq
0x10258826c <+60>:  stp    x9, x0, [sp, #0x10]
0x102588270 <+64>:  adrp   x9, 63
0x102588274 <+68>:  add    x9, x9, #0xe52            ; "BUG IN CLIENT OF LIBDISPATCH: Assertion failed: "
0x102588278 <+72>:  stp    x9, x8, [sp]
0x10258827c <+76>:  adrp   x1, 63
0x102588280 <+80>:  add    x1, x1, #0xe1d            ; "%sBlock was %sexpected to execute on queue [%s (%p)]"
0x102588284 <+84>:  sub    x0, x29, #0x18
0x102588288 <+88>:  bl     0x1025c3d60               ; symbol stub for: asprintf
0x10258828c <+92>:  ldur   x19, [x29, #-0x18]
0x102588290 <+96>:  str    x19, [sp]
0x102588294 <+100>: adrp   x0, 63
0x102588298 <+104>: add    x0, x0, #0xe88            ; "%s"
0x10258829c <+108>: bl     0x1025bfed4               ; _dispatch_log
0x1025882a0 <+112>: adrp   x8, 101
0x1025882a4 <+116>: str    x19, [x8, #0x1c0]

-> 0x1025882a8 <+120>: brk #0x1

I think the problem here is very related. However there are no closures where I could add @Sendable ;-)

Written by chnbr in 822920022
Indeed @Sendable solved the problem with the speech recognition permission.

Yay!

Written by chnbr in 822920022
Now another problem probably related to Apple Watch WCSession has occurred.

I recommend that you put the details of that into a new thread. I don’t want this thread to become the place for all Swift concurrency problems, largely because the DevForums platform isn’t great at dealing with long threads )-:

Use the App & System Services > Processes & Concurrency topic area and tag it with Swift and Concurrency. That way I’ll be sure to see it.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Request authorization for the notification center crash iOS app on Swift 6
 
 
Q