Here's an example of the goal call site I'd like to have:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
Task {
do {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let token = try await appDelegate.fetchAPNsToken()
print("Found the token! \(token)")
// Oo now I can do fancy async await network calls to the server to register this APNs token with!
} catch {
print("Error getting APNs token: \(error)")
}
}
}
}
The flow for registering for APNs token is a little strange (probably because it's from iPhone OS 3.0). It uses delegates, so we have to store our continuation, but on top of this, we A) can't control the responding delegate object, it has to be UIApplication, and B) multiple requests for an APNs token can theoretically come in at the same time, and UIApplication seemingly (and only sometimes!) coalesces that into a single delegate answer, so there's not necessarily a 1:1 mapping with "calling a method" and "getting a delegate response".
Anyway so here are the two solutions I tried.
The first doesn't work, the second does. With the first I tried to go right to async await from the delegate method, but I keep getting "Mutation of captured var 'self' in concurrently-executing code" as a compiler error and I don't know how to get past that. See the ❌ below.
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
return true
}
var continuations: [CheckedContinuation<String, Error>] = []
func fetchAPNsToken() async throws -> String {
return try await withCheckedThrowingContinuation { continuation in
let options: UNAuthorizationOptions = [.badge, .alert, .sound, .providesAppNotificationSettings]
UNUserNotificationCenter.current().requestAuthorization(options: options) { [weak self] isGranted, error in
guard isGranted else {
continuation.resume(throwing: error!)
return
}
DispatchQueue.main.async {
self.continuations.append(continuation) // ❌ Mutation of captured var 'self' in concurrently-executing code
UIApplication.shared.registerForRemoteNotifications()
}
}
}
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let token = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
continuations.forEach { $0.resume(returning: token) }
continuations.removeAll()
}
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
continuations.forEach { $0.resume(throwing: error) }
continuations.removeAll()
}
}
So I tried to write a callback style wrapper instead, which I then converted to async await. This seems to work.
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
return true
}
var handlers: [(Result<String, Error>) -> Void] = []
// Callback version
func fetchAPNsToken(completion: @escaping (Result<String, Error>) -> Void) {
let options: UNAuthorizationOptions = [.badge, .alert, .sound, .providesAppNotificationSettings]
UNUserNotificationCenter.current().requestAuthorization(options: options) { [weak self] isGranted, error in
guard isGranted else {
completion(.failure(APNsTokenError.notGranted))
return
}
DispatchQueue.main.async {
self?.handlers.append(completion)
UIApplication.shared.registerForRemoteNotifications()
}
}
}
// Async await version that leverages the above callback version
func fetchAPNsToken() async throws -> String {
return try await withCheckedThrowingContinuation { continuation in
fetchAPNsToken { result in
switch result {
case .success(let token):
continuation.resume(returning: token)
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let token = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
handlers.forEach { $0(.success(token)) }
handlers.removeAll()
}
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
handlers.forEach { $0(.failure(error)) }
handlers.removeAll()
}
}
So how'd I do? Should I just use the callback version? Is there hope of doing something different with the delegate version directly? Did I commit some horrible threading mistake?
@christianselig Well, that was written poorly 😓 I meant, in the current example,
fetchAPNsToken()
needs to be called, neverUIApplication.shared.registerForRemoteNotifications()
in order to set up the continuation(s).registerForRemoteNotifications
is not deprecated in any way (that I know of)