Async Request Does Not Enter Completion Block

Async request does not enter completion block

My hunch is that the queue is being deallocated before the request is completed and that retain cycles are involved

Not quite. Retain cycles are not involved. Persistence is involved. You are doing this in a main function. It exits immediately - the asynchronous stuff (the networking and the subsequent callback) is asynchronous, so it would come later, if we had any persistence. But we don't. main exits, and that means that the whole darned program is torn down, kaboom, before there is any opportunity to do any networking, let alone call back into the completion handler after the networking.

Now contrast this with how things happen in real life. In a real iOS app, main does not exit, because it calls UIApplicationMain, which loops until the app is terminated.

int main(int argc, char *argv[])
{
@autoreleasepool {
return UIApplicationMain(argc, argv, nil,
NSStringFromClass([AppDelegate class]));
}
}

In that code, UIApplicationMain just keeps running until it is aborted or otherwise terminated. Meanwhile, classes and instance have sprung to life, and they persist, because UIApplicationMain does not stop. For example:

@implementation MyViewController
- (void) someMethod {
// ...
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
// ...
}
}
@end

Now, in one sense, exactly the same thing happens: someMethod exits immediately. But our program overall is still running! UIApplicationMain has a run loop and that run loop is still cycling. Thus, things live on, and so now the asynchronous material can happen - we can network and then call the callback.

Adding a completion handler to an asynchronous request method that only returns completion

+animateWithDuration:animations: of UIView is a simple example on how declare an void block.

It's declared like this:

+ (void)animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations;

I personally never remember how to declare them, I alway check on an iOS SDK (I know a few methods) to remember the way of declaring them.

So applying to your question:

+ (void)requestAuthToken:(void (^)(void))block
{
//do your thing

//When it's okay:
if (block)
{
block();
}
}

On your other call (code taken from your other code):

-(void)requestDataForUser
{
//Do your things

//Need to refresh token:
[[CorrectClassName requestAuthToken:^{
[weakSelf requestDataForUser];
}];
}

Swift async method and return / completion block

From my point of view it is possible to use throws to simplify interaction with method. I write small example. With this implementation it is quite easy not to miss error because of the exceptions. And clear response status will show if request is successful.

Throws errors:

enum LoginError: ErrorType {
case missingEmail
case missingPassword
}

Response status:

enum LoginRequestStatus {
case noUserFound
case invalidPassword
case success
}

Function implementation:

func login(withEmail email: String, password: String) throws -> LoginRequestStatus {

guard email.characters.count > 0 else{
throw LoginError.missingEmail
}

guard password.characters.count > 0 else{
throw LoginError.missingPassword
}

// do some request

return LoginRequestStatus.success
}

Swift program never enters CompletionHandler for a dataTask

It appears you're writing this in a command-line app. In that case the program is terminating before the URLRequest completes.

I think the issue may be dealing with synchronicity, and perhaps I need to use a Semaphore, but I want to make sure I'm not missing anything obvious first.

Exactly.

The typical tool in Swift is DispatchGroup, which is just a higher-level kind of semaphore. Call dispatchGroup.enter() before starting the request, and all dispatchGroup.leave() at the end of the completion handler. In your calling code, include dispatchGroup.wait() to wait for it. (If that's not clear, I can add code for it, but there are also a lot of SO answers you can find that will demonstrate it.)

Preventing completion handler to be executed synchronously

I'm working on the assumption that you want to be able to add multiple callbacks that will all be run when the latest request completes, whether it was already in-flight or not.

Here's a sketch of a solution. The basic point is to take a lock before touching the array(s) of handlers, whether to add one or to invoke them after the request has completed. You must also synchronize the determination of whether to start a new request, with the exact same lock.

If the lock is already held in the public method where the handlers are added, and the request's own completion runs, then the latter must wait for the former, and you will have deterministic behavior (the new handler will be invoked).

class WhateverRequester
{
typealias SuccessHandler = (Whatever) -> Void
typealias FailureHandler = (Error) -> Void

private var successHandlers: [SuccessHandler] = []
private var failureHandlers: [FailureHandler] = []

private let mutex = // Your favorite locking mechanism here.

/** Flag indicating whether there's something in flight */
private var isIdle: Bool = true

func requestWhatever(succeed: @escaping SuccessHandler,
fail: @escaping FailureHandler)
{
self.mutex.lock()
defer { self.mutex.unlock() }

self.successHandlers.append(succeed)
self.failureHandlers.append(fail)

// Nothing to do, unlock and wait for request to finish
guard self.isIdle else { return }

self.isIdle = false
self.enqueueRequest()
}

private func enqueueRequest()
{
// Make a request however you do, with callbacks to the methods below
}

private func requestDidSucceed(whatever: Whatever)
{
// Synchronize again before touching the list of handlers and the flag
self.mutex.lock()
defer { self.mutex.unlock() }

for handler in self.successHandlers {
handler(whatever)
}

self.successHandlers = []
self.failureHandlers = []
self.isIdle = true
}

private func requestDidFail(error: Error)
{
// As the "did succeed" method, but call failure handlers
// Again, lock before touching the arrays and idle flag.
}
}

This is so broadly applicable that you can actually extract the callback storage, locking, and invocation into its own generic component, which a "Requester" type can create, own, and use.

How to know when all async functions have completed?

You generally use dispatch groups for this sort of thing. The trick here is that if you need to wait for disconnectFromSession to call its completion handler, then need to have didDisconnectFromSession call leave for the dispatch group.

So create ivar for the dispatch group:

let group = DispatchGroup()

Have your pauseStream use this DispatchGroup to call its completion handler when enter calls are offset by their corresponding leave calls:

func pauseStream(completion: @escaping () -> ()) {
group.enter()
disconnectFromSession() // this will call `leave` in its delegate method

model.publishers = []

group.enter()
model.someAsynchronousMethod() { result in
defer { group.leave() }

...
}

group.notify(queue: .main) {
completion()
}
}

And, your didDisconnectFromSession, would call that corresponding leave:

func didDisconnectFromSession(...) {
group.leave()

...
}

Syntax to wrap a completion handler in dispatch async

There isn't a built-in syntax for expressing that, but you can always define a generic function or operator to enable something along those lines.

For example:

infix operator >

func ><T>(left:@escaping (T)->(), right:DispatchQueue) -> (T)->() {
return { input in
right.async { left(input) }
}
}

With the above custom operator defined, your code can be:

request.completionBlock = completionBlock > DispatchQueue.main

which I think is the general feel you are looking for.

async requests on Swift using completion handler and DispatchSemaphore

Is your intention here to create a synchronous function that will block and wait until the async request completes?

If so, you're almost there but you need to change the Semaphore to start with 0 rather than 1 (decrementing from 1 to 0 won't stop execution, it needs to go negative), and you need to move the wait() call outside the closure, otherwise you aren't stopping the function from returning and instead would be blocking your closure from ever completing.

func customRequest(zipCode: String) -> Bool {
var response = false

let dispatchQueueProcess = DispatchQueue.global(qos: .userInitiated)
let semaphore = DispatchSemaphore(value: 0)

// Start async work on background thread, current function's thread
// execution point will then immediately move to the line
// after the closing brace
dispatchQueueProcess.async {
apiRequest(zipCode: zipCode) { apiResponse in
if let apiResponse = apiResponse {
response = apiResponse
} else {
print("Server did not Response")
}

// Tell the original function's thread that it is OK to continue
semaphore.signal()

}
}

// Immediately wait for the semaphore after starting the async work so that
// the customRequest(zipCode:) function pauses here until the semaphore is
// signalled in the closure.
semaphore.wait()

// Once the semaphore.signal() call happens in the async closure execution
// resumes, and the response variable can now be returned with the updated
// value.
return response
}

You might find that you don't actually want to make a synchronous function like this, because if you call this from the main queue you will freeze the UI until the response is received.

Edit: This example should copy-paste run in a Playground

import Foundation

// template function to simulate the API call
func apiRequest(zipCode: String, response: (Bool?)->Void) {
sleep(3)
response(true)
}

func customRequest(zipCode: String) -> Bool{
var response = false

let dispatchQueueProcess = DispatchQueue.global(qos: .userInitiated)
let semaphore = DispatchSemaphore(value: 0)

dispatchQueueProcess.async {
apiRequest(zipCode: zipCode) { apiResponse in
if let apiResponse = apiResponse {
response = apiResponse
} else {
print("Server did not Response")
}
semaphore.signal()

}
}

semaphore.wait()

return response
}

print("Result: \(customRequest(zipCode: "90030"))")


Related Topics



Leave a reply



Submit