Wait for completion handler to finish - Swift
I am trying to check if UserNotifications are enabled and if not I want to throw an alert. So I have a function checkAvailability
which checks multiple things, including the UserNotification authorization status.
func checkAvailabilty() -> Bool {
//
// other checking
//
var isNotificationsEnabled = false
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound], completionHandler: { (granted, error) in
if granted {
isNotificationsEnabled = true
}
else {
isNotificationsEnabled = false
}
})
}
if isNotificationsEnabled {
return true
}
else {
// Throw alert: Remind user to activate notifications
return false
}
}
But the completion handler gets called too late. The function already returned false
and after that the code in the colsure executes.
I tried to put the whole statement UNUserNotificationCenter.current().requestAuthorization()
in a synchronous dispatch queue but this didn't work.
Another approach would be to return from inside the closure but I have no idea how to accomplish that.
Do not wait, use a completion handler, for convenience with an enum:
enum AuthResult {
case success(Bool), failure(Error)
}
func checkAvailabilty(completion: @escaping (AuthResult) -> ()) {
//
// other checking
//
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound], completionHandler: { (granted, error) in
if error != nil {
completion(.failure(error!))
} else {
completion(.success(granted))
}
})
}
And call it:
checkAvailabilty { result in
switch result {
case .success(let granted) :
if granted {
print("access is granted")
} else {
print("access is denied")
}
case .failure(let error): print(error)
}
}
In Swift 5.5 with async/await it does wait indeed
func checkAvailabilty() async throws -> Bool {
//
// other checking
//
return try await UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound])
}
And call it:
Task {
do {
let granted = try await checkAvailabilty()
if granted {
print("access is granted")
} else {
print("access is denied")
}
} catch {
print(error)
}
}
Yeah. So as you figured what is happening here is that the function returns before the completion handler gets called. So what you want to do is pass an asynchronous callback to the checkAvailability
function so it will callback once the completion handler is fired.
func checkAvailability(callback: @escaping (Bool) -> Void) {
//
// other checking
//
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound], completionHandler: { (granted, error) in
if granted {
callback(true)
} else {
callback(false)
}
})
}
you would call this function like so...
checkAvailability(callback: { (isAvailable) -> Void in
if isAvailable {
// notifications are available
} else {
// present alert
}
})
Keep in mind that when you go to present the alert you may need to explicitly dispatch the call to the main thread since the completion handler may callback on a different thread. In which case this is how you would want to call the function and present the alert...
checkAvailability(callback: { (isAvailable) -> Void in
if isAvailable {
// notifications are available
} else {
DispatchQueue.main.async {
// present alert
}
}
})