Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Wait for asynchronous request result

I would like somehow to asynchronously validate the pin in ABPadLockScreen since pins are not saved on the device. I'm using Alamofire for http requests along with PromiseKit to have promises.

I have tried to use AwaitKit but the problem is that i get into a deadlock.

I have also tried to use semaphore as well, but the result is the same. Since i can't change the ABPadLock method to accommodate something like a completion handler i need some solution, it doesn't matter if it blocks the main thread, just that it works.

Alamofire request method:

public func loginAsync(pinCode: String?, apiPath: String?) -> Promise<LoginResult>{
    return Promise { fullfil, reject in
        let params = [
            "Pin": pinCode!
        ]

        Alamofire.request(.POST, "\(baseUrl!)/\(apiPath!)", parameters: params).responseObject{(response: Response<LoginResult, NSError>) in
            let serverResponse = response.response

            if serverResponse!.statusCode != 200 {
                reject(NSError(domain: "http", code: serverResponse!.statusCode, userInfo: nil))
            }

            if let loginResult = response.result.value {
                fullfil(loginResult)
            }
        }
    }
}

ABPadLockScreen pin validation method:

public func padLockScreenViewController(padLockScreenViewController: ABPadLockScreenViewController!, validatePin pin: String!) -> Bool {
    let pinCode = pin!
    let defaults = NSUserDefaults.standardUserDefaults()
    let serverUrl = defaults.stringForKey(Util.serverUrlKey)
    let service = AirpharmService(baseUrl: serverUrl)

    service.loginAsync(pinCode, apiPath: "sw/airpharm/login").then { loginResult -> Void in
        if loginResult.code == HTTPStatusCode.OK {
            AirpharmService.id = loginResult.result!.id
        }
    }

    return false // how do i get the result of above async method here?
}

With semaphore:

public func padLockScreenViewController(padLockScreenViewController: ABPadLockScreenViewController!, validatePin pin: String!) -> Bool {

    var loginResult: LoginResult?

    let defaults = NSUserDefaults.standardUserDefaults()

    let baseUrl = defaults.stringForKey(Util.serverUrlKey)

    let service = AirpharmService(baseUrl: baseUrl)

    let semaphore: dispatch_semaphore_t = dispatch_semaphore_create(0)

    service.loginAsync(pin, apiPath: "sw/airpharm/login").then { loginResultRaw -> Void in
        loginResult = loginResultRaw
        dispatch_semaphore_signal(semaphore)//after a suggestion from Josip B.
    }

    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)

    return loginResult != nil // rudimentary check for now
}

EDIT:

After a suggestion from Josip B. i added semaphore signal in then, but it still doesn't work

AirpharmService is a class that contains a static property called id, and the Alamofire request method.

ABPadLockScreen pin validation is done on main thread in a ViewController

SOLVED EDIT:

Thanks to everyone for being so patient with me and my, not so good, knowledge of swift and iOS. There are a lot of good answers here and in the end i just went with, in my opinion, simplest solution. I listened to Losiowaty-s suggestion; implemented a spinner and manually dismissed the lock screen when i get the response from the server. I've used a SwiftSpinner. The final solution looked like this:

public func padLockScreenViewController(padLockScreenViewController: ABPadLockScreenViewController!, validatePin pin: String!) -> Bool {
    let defaults = NSUserDefaults.standardUserDefaults()
    let baseUrl = defaults.stringForKey(Util.serverUrlKey)

    let service = AirpharmService(baseUrl: baseUrl)
    SwiftSpinner.show("Logging in. Please wait...")
    service.loginAsync(pin, apiPath: "sw/airpharm/login").then { loginResult -> Void in
        if loginResult.code == HTTPStatusCode.OK {
            SwiftSpinner.hide()
            AirpharmService.id = loginResult.result!.id
            self.unlockWasSuccessfulForPadLockScreenViewController(padLockScreenViewController)
        } else if loginResult.code == HTTPStatusCode.Unauthorized {
            let toast = JLToast.makeText("Invalid pin, please try again", duration: 5)
            toast.show()
            SwiftSpinner.hide()
        } else {
            let toast = JLToast.makeText("\(loginResult.code) sent from server. Please try again.", duration: 5)
            toast.show()
            SwiftSpinner.hide()
        }
    }.error { error in
        let toast = JLToast.makeText("\((error as NSError).code) sent from server. Please try again.", duration: 5)
        toast.show()
        SwiftSpinner.hide()
    }

    return false
}
like image 795
Nikola.Lukovic Avatar asked Jul 31 '16 13:07

Nikola.Lukovic


People also ask

How do I wait for async results?

async and await Inside an async function, you can use the await keyword before a call to a function that returns a promise. This makes the code wait at that point until the promise is settled, at which point the fulfilled value of the promise is treated as a return value, or the rejected value is thrown.

Does async wait for response?

Asynchronous requests will wait for a timer to finish or a request to respond while the rest of the code continues to execute.

Which is the recommended way to wait for an async method to complete?

No problem, just make a loop and call this function with an await: [code] for (int i = pendingList. Count - 1; i >= 0; i--)

How do you wait for async to finish JavaScript?

The await operator is used to wait for a Promise. It can be used inside an Async block only. The keyword Await makes JavaScript wait until the promise returns a result. It has to be noted that it only makes the async function block wait and not the whole program execution.


4 Answers

It's great that a lot of people tried to help you make your asynchronous call synchronous. Personally I agree with @OOPer and his comment that you should redesign your code, especially after looking through ABPadLockScreen code. It seems they don't support asynchronous pin verification, which is a shame. Also it seems from their github repo that the original author has abandoned the project, for the time being at least.

I'd attempt to solve your issue like this :

public func padLockScreenViewController(padLockScreenViewController: ABPadLockScreenViewController!, validatePin pin: String!) -> Bool {
    let pinCode = pin!
    let defaults = NSUserDefaults.standardUserDefaults()
    let serverUrl = defaults.stringForKey(Util.serverUrlKey)
    let service = AirpharmService(baseUrl: serverUrl)

    service.loginAsync(pinCode, apiPath: "sw/airpharm/login").then { loginResult -> Void in
        if loginResult.code == HTTPStatusCode.OK {
            AirpharmService.id = loginResult.result!.id
            self.handleLoginOk()
        } else {
            self.handleLoginFailed()
        }
    }

    // disable interaction on padlock screen 
    // indicate to user that an async operation is going on, show a spinner

    return false // always return false here
}

func handleLoginOk() {
    // dismiss the ABPadlockScreenViewController manually
}

func handleLoginFailed() {
    // dismiss the spinner indicating the async operation
    // restore user interaction to padlock screen
}

With this approach your users will know that something is going on (the spinner, you can use for example SVProgressHUD as a drop-in solution) and that the app didn't hang. It is quite important, ux-wise, as users with poor connection could get frustrated thinking the app hanged and close it.
There is a potential problem though - if the padlock screen shows some kind of "wrong pin" message when you return false from the delegate method, it could be visible to the user creating some confusion. Now this can be tackled by making/positioning the spinner so that it obscures the message, though this is a very crude and unelegant solution. On the other hand, maybe it can be customised enough so that no message gets shown, and you'd display your own alert after server side verification.

Let me know what you think about this!

like image 171
Losiowaty Avatar answered Oct 22 '22 11:10

Losiowaty


... it doesn't matter if it blocks the main thread... but the problem is that i get into a deadlock.

One problem could be it is blocking the main thread with dispatch_semaphore_wait, so the Alamofire response never get a chance to run on the main thread and you're deadlocking.

The solution to this could be create another queue on which the Alamofire completion handler is dispatched.

For example:

If you making a request like this:

Alamofire.request(.GET, "https://jsonplaceholder.typicode.com/posts").validate().responseData() { response in
    print(response.result.value)
}

You can modify this call to dispatch completion handler in your defined queue like this:

let queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT)

let request = Alamofire.request(.GET, "https://jsonplaceholder.typicode.com/posts", parameters: .None).validate()
request.response(queue: queue, responseSerializer: Request.JSONResponseSerializer(options: .AllowFragments)) { response in
    print(response.result.value)
}

A simplified version for test.

//MARK: Lock Screen Delegate
func padLockScreenViewController(padLockScreenViewController: ABPadLockScreenViewController!, validatePin pin: String!) -> Bool {
    print("Validating Pin \(pin)")

    let queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT)
    let semaphore = dispatch_semaphore_create(0)

    let request = Alamofire.request(.GET, "https://jsonplaceholder.typicode.com/posts", parameters: .None).validate()
    request.response(queue: queue, responseSerializer: Request.JSONResponseSerializer(options: .AllowFragments)) { response in
        print(response.result.value)
        //isPinValid = ???
        dispatch_semaphore_signal(semaphore);
    }

    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)

    return thePin == pin
    //return isPinValid
}
like image 43
Warif Akhand Rishi Avatar answered Oct 22 '22 13:10

Warif Akhand Rishi


Try this:

add dispatch_group:

static let serviceGroup = dispatch_group_create();

Then after calling the function, wait for this group:

public func padLockScreenViewController(padLockScreenViewController: ABPadLockScreenViewController!, validatePin pin: String!) -> Bool {

    var loginResult: LoginResult?

    let defaults = NSUserDefaults.standardUserDefaults()

    let baseUrl = defaults.stringForKey(Util.serverUrlKey)

    let service = AirpharmService(baseUrl: baseUrl)

    let semaphore: dispatch_semaphore_t = dispatch_semaphore_create(0)

    service.loginAsync(pin, apiPath: "sw/airpharm/login").then { loginResultRaw -> Void in
        loginResult = loginResultRaw
    }

    dispatch_group_wait(yourClass.serviceGroup, DISPATCH_TIME_FOREVER);

    return loginResult != nil // rudimentary check for now
}

And release the group after the function returns an answer:

public func loginAsync(pinCode: String?, apiPath: String?) -> Promise<LoginResult>{
    return Promise { fullfil, reject in
        let params = [
            "Pin": pinCode!
        ]

        Alamofire.request(.POST, "\(baseUrl!)/\(apiPath!)", parameters: params).responseObject{(response: Response<LoginResult, NSError>) in
            let serverResponse = response.response

            if serverResponse!.statusCode != 200 {
                reject(NSError(domain: "http", code: serverResponse!.statusCode, userInfo: nil))
            }

            if let loginResult = response.result.value {
                fullfil(loginResult)
            }
            dispatch_group_leave(yourClass.serviceGroup)
        }
    }
}
like image 41
Roee84 Avatar answered Oct 22 '22 13:10

Roee84


Based on the comments we exchanged, it sounds like the endless wait when you tried using a semaphore is because the semaphore signal is never being sent. Let's try to simplify this down to the minimum code needed to test:

public func padLockScreenViewController(padLockScreenViewController: ABPadLockScreenViewController!, validatePin pin: String!) -> Bool {

    var success = false
    let defaults = NSUserDefaults.standardUserDefaults()
    let baseUrl = defaults.stringForKey(Util.serverUrlKey)
    let semaphore: dispatch_semaphore_t = dispatch_semaphore_create(0)

    let params = ["Pin": pin]

    Alamofire.request(.POST, "\(baseUrl!)/sw/airpharm/login", parameters: params).responseObject {
        (response: Response<LoginResult, NSError>) in

        if let loginResult = response.result.value where loginResult.code == HTTPStatusCode.OK {
            AirpharmService.id = loginResult.result!.id
            success = true
        }

        dispatch_semaphore_signal(semaphore)
    }

    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
    return success
}

This should either:

  1. crash because you are force unwrapping several variables (e.g.baseUrl!, loginResult.result!.id, etc. and one of them is nil

  2. return true if you got a valid LoginResult

  3. return false if you didn't get a valid LoginResult

But theoretically, it shouldn't deadlock.

like image 36
Daniel Hall Avatar answered Oct 22 '22 12:10

Daniel Hall