Wait for completion handler to finish - Swift
Asked Answered
B

4

9

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.

Beaubeauchamp answered 15/2, 2017 at 16:52 Comment(1)
Add a completion handler as a parameter to checkAvailabilty() and call it at the end of requestAuthorization's completion handler.Uncleanly
B
18

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)
    }
 }
Bridges answered 15/2, 2017 at 17:1 Comment(5)
Thanks worked perfectly! But one question: What exactly does the @escaping mean?Beaubeauchamp
The closure is passed as an argument to a function and it is invoked after the function returns, that means the closure is escapingBridges
So when a closure with the @escaping argument gets called, the function in this case checkAvailability automatically returns?Beaubeauchamp
@Beaubeauchamp Sort of.Bridges
Ok thanks, I've read a little bit more about escaping and noescape and think I understand it. But what I don't understand is why is it necessary to mark this closure as escaping?Beaubeauchamp
A
5

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
            }
        }
    })
Asperse answered 15/2, 2017 at 16:59 Comment(1)
Thanks for your answer and for your advice to make UI modifications explicitly in the main thread with DispatchQueue.main.async{...}Beaubeauchamp
H
0

The code block

if isNotificationsEnabled {
    return true
}
else {
    // Throw alert: Remind user to activate notifications
    return false
}

gets called immediately after the call to requestAuthorization(options:completionHandler).

You should instead display the alert from within the completion handler:

UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound], completionHandler: { (granted, error) in
    if !granted {
        // Show alert
    }
})

Your function checkAvailability is no longer synchronously returning a Bool, as the call to requestAuthorization(options:completionHandler) is asynchronous.

Hydra answered 15/2, 2017 at 17:2 Comment(1)
Thanks for your answer but this doesn't solve the problem that I have to return a bool value. That's why I went with the other solution.Beaubeauchamp
M
0

Another alternative is to return two parameters in the completion handler:

func checkAvailabilty(completion: @escaping (_ granted: Bool, _ error: Error?) -> ()) {
    UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { granted, error in
        completion(granted, error)
    }
}

usage

checkAvailabilty { granted, error in
    guard error == nil else {
        // An Authorization error has occurred. Present an alert to the user with the error description.
        DispatchQueue.main.async {
            let alert = UIAlertController(title: "Alert", message: error?.localizedDescription ?? "Authorization failed. Unknown error.", preferredStyle: .alert)
            alert.addAction(UIAlertAction(title: "OK", style: .default))
            self.present(alert, animated: true)
        }
        return
    }
    if granted {
        print("granted")  // authorization was successful
    } else {
        print("denied")  // present alert from the main thread
        DispatchQueue.main.async {
            let alert = UIAlertController(title: "Attention", message: "The App needs you to turn on notifications !!!", preferredStyle: .alert)
            alert.addAction(UIAlertAction(title: "OK", style: .default))
            self.present(alert, animated: true)
        }
    }
}
Mauriciomaurie answered 18/11, 2017 at 9:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.