Wait for completion handler to finish - Swift

2019-03-28 09:03发布

问题:

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.

回答1:

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)
    }
}


回答2:

Ya so as you figured what is happening 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 checkAvailabilty(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
            }
        }
    })


回答3:

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.



回答4:

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)
        }
    }
}