Chaining closures and completion handlers Swift 3 - swift

I'm having a hard time understanding how chaining completion handlers with closures works.
The example I'm working with is the following :
typealias CompletionHandler = (_ result: AnyObject?, _ error: NSError?) -> Void
func getRequestToken(completionHandler: CompletionHandler){
taskForGET(path: "PathToWebsite.com") { (result, error) in
if let error = error {
print(error)
} else {
print(result)
}
}
}
func taskForGET(path: String, completionHandler: CompletionHandler) {
//URLSESSIONCODE with completion handler
(data, response, error) {
if let error = error {
CompletionHandler(result: nil, error: error)
} else {
let data = data
parseJSON(data: data, completionHandler: completionHandler)
}
}
}
func parseJSON(data: NSData, completionHandler: CompletionHandler) {
//SerializingJSON with do catch block
do {
completionHandler(result: theResult, error: nil)
} catch {
completionHandler(result: nil, error: error)
}
}
So basically what this code does is it kicks off a GET request to a server. If the GET request sends back data, then it parses it into JSON. If at any point along the way something fails, it returns an error.
I understand basically what is going on here, but I don't understand how the completion handlers are being fired off.
First taskForGET is called which has a completion handler as a parameter that can return a result or an error, I've got that.
The next step is calling parseJSON, where the data from taskForGET is passed but then the completionhandler that's being passed is taskForGET's completion handler. I don't understand this at all.
Then down in parseJSON, the completion handler either returns JSON or an error by calling the completion handler from its parameters..which in this case is taskForGET's completion handler?
I don't understand the flow. Even once we've parsed JSON successfully, how does calling taskForGET's result ever get back up to getRequestToken.
Any help with this would be appreciated.

There is only one completion handler which is passed from one method to another.
Lets declare the handler separately, btw. in Swift 3 omit the parameter labels in the type alias:
typealias CompletionHandler = (Any?, Error?) -> Void
let handler : CompletionHandler = { (result, error) in
if let error = error {
print(error)
} else {
print(result)
}
}
This closure is supposed to be executed at the end of the process. Now the getRequestToken method looks like
func getRequestToken(completionHandler: CompletionHandler){
taskForGET(path: "PathToWebsite.com", completionHandler: handler)
}
The handler / closure is passed as a parameter from getRequestToken to taskForGET and then from taskForGET to parseJSON but it's always the same object.

Related

Does a Completion Handler end the function?

Perhaps I do not understand the concept of a completion handler, but I have a function like so;
func someFunction(completion: #escaping (Bool, LoginError?) -> Void) {
self.checkDevice() { allowed, error in
if let e = error {
completion(false, e)
}
completion(true, nil)
}
}
While being light on what checkDevice() does, the basic premise is that it performs an asynchronous network call, and returns either true with no error (nil), or returns false with an error.
When I run this code, I am finding that the completion handler is being called twice. It sends a completion as a tuple (as false, error) and also as (true, nil). I've done some debugging and there seems to be no manner in which someFunction() is called twice.
I was of the belief that once a completion is sent, the function would end. In my test case, I am forcing an error from checkDevice(), which should result in me sending the completion as (false, error), but I am seeing both (false, error) and (true, nil). Does a completion not immediately end the function?
I was of the belief that once a completion is sent, the function would end.
No, why would that be? When you call this function it’s like calling any other function. The name has no magic power.
Rewrite it like this:
func someFunction(completion: #escaping (Bool, LoginError?) -> Void) {
self.checkDevice() { allowed, error in
if let e = error {
completion(false, e)
return // *
}
completion(true, nil)
}
}
Actually, I should mention that this is a poor way to write your completion handler. Instead of taking two parameters, a Bool and an Optional LoginError to be used only if the Bool is false, it should take one parameter — a Result, which carries both whether we succeeded or failed and, if we failed, what the error was:
func someFunction(completion: #escaping (Result<Void, Error>) -> Void) {
self.checkDevice() { allowed, error in
completion( Result {
if let e = error { throw e }
})
}
}
As you can see, using a Result as your parameter allows you to respond much more elegantly to what happened.
Indeed, checkDevice itself could pass a Result into its completion handler, and then things would be even more elegant (and simpler).
A completion handler does not end a function. The typical way to end a function is to insert a return.
See comment re: your specific case
Consider including parameter names (for reference). And the completion should only be called once. You can do that using return or by using a full if-else conditional.
func someFunction(completion: #escaping (_ done: Bool, _ error: LoginError?) -> Void) {
checkDevice() { (allowed, error) in
if let e = error {
completion(false, e)
} else {
completion(true, nil)
}
}
}
func someFunction(completion: #escaping (_ done: Bool, _ error: LoginError?) -> Void) {
checkDevice() { (allowed, error) in
if let e = error {
completion(false, e)
return
}
completion(true, nil)
}
}
By giving parameters names, when calling the function, you can now reference the signature to denote the meanings of its parameters:
someFunction { (done, error) in
if let error = error {
...
} else if done {
...
}
}

Variable outside DispatchQueue.main.async is empty

I tried to resolve this error since days but I dont understand why am I getting this error in the first place.
Please help...
func createData(request:Crudpb_CreateRequest) -> String {
DispatchQueue.main.async {
self.response = try! self.client.create(request)
print("This is response 1: " + self.response.result) // <-------- This is priting the right response
}
print("This is response outside DispatchQueue: " + self.response.result) // <------- This is not printing anyvalue
return self.response.result // <------ This is not
}
You are dispatching whatever work you are doing in your create request method asynchronously, therefor your create data function will not wait for this work to be done to continue its execution, it just calls it and keeps its execution and thats why your value is not modified when you reach your "This is response outside DispatchQueue: " statement.
It will be modified in the capture block that you have created, thats why you need to create an #escaping completion block like they mentioned before, to only return your value when the work you did to obtain it is finished.
func createData(request:Crudpb_CreateRequest, with completion: #escaping (String) -> Void) {
DispatchQueue.main.async {
self.response = try! self.client.create(request)
completion(self.response)
}
}
This is the way I use #escaping closures: You don't need a return value in the function, given that the execution of the calling function may end before the closure is finished. You also need to specify a dataType in the #escaping parameter (Bool in this case)... also, Function types cannot have argument labels, so you must use "_"
self.fetchStuff(onCompletion: { (success) in
if success {
// Do something
}
})
func fetchStuff(onCompletion: #escaping (_ success: Bool) -> Void) {
// Do some asynch stuff
onCompletion(true)
}

Swift Completion Handler Not Called on Second Call to Function

I have a Swift function with a completion handler that I want to be called when a certain network request is done. The network request can be repeated based on if the server response says there is more data (offset is provided).
I initially call it like this:
func myMainFunction(){
getTasks(project: project, paginationToken: nil) {
print("This never fires...")
}
}
And my getTasks() function is like this:
func getTasks(project: Project, paginationToken: String?, completion: #escaping () -> Void){
var url = "..."
if let token = paginationToken{
url += "&offset=\(token)"
}
Alamofire.request(url).responseJSON { response in
switch response.result {
case .success(let value):
let json = JSON(value)
//...
//Check for pagination
let offset = json["next_page"]["offset"].stringValue
if !offset.isEmpty{
//Theres a pagination token available, go again!
getTasks(project: project, paginationToken: offset){}
}else{
print("This prints to the log on the last run through")
completion()
}
case .failure(let error):
print("Task error: \(error)")
}
}
}
If there is never an offset then the completion() handler works fine. But if getTasks() has to be called again, the completion handler is somehow invalidated and never returns to my original myMainFunction().
Any ideas?
You are passing empty closure if offset is not empty. You need to forward the completion param to getTasks again. Try this.
//...
if !offset.isEmpty {
getTasks(project: project, paginationToken: offset, completion: completion)
}
//...

Calling a function that has a TypeAlias as a parameter?

So I've written a little practice program that has to do with closures. I'm trying to better understand how the asynchronous concept works. When I try to call request(), I get conversion errors as seen below:
import UIKit
let correctPasscode = "3EyX"
typealias CompletionHandler = (result: AnyObject?, error: String?) -> Void
func request(passcode: String, completionHandler: CompletionHandler) {
sendBackRequest(passcode) {(result, error) -> Void in
if error != nil {
print(error)
}
else {
print(result)
}}
}
func sendBackRequest(passCode: String, completionHandler: CompletionHandler) {
if passCode == correctPasscode {
completionHandler(result: "Correct. Please proceed", error: nil)
} else {
completionHandler(result: nil, error: "There was an error signing in")
}
}
request(correctPasscode, completionHandler: CompletionHandler) // Error happens here
Type alias is there to tell you what actual type you need to pass. In this case, the type is a closure of type
(result: AnyObject?, error: String?) -> Void
You pass it like this:
request(correctPasscode, completionHandler:{
(result: AnyObject?, error: String?) in
print("Inside the handler...")
// Do some useful things here
})
or even shorter -
request(correctPasscode) {
(result: AnyObject?, error: String?) in
print("Inside the handler...")
// Do some useful things here
}
or even shorter - (the types are known via the func declaration) -
request(correctPasscode) { result, error in
print("Inside the handler...")
// Do some useful things here
}
I'm not sure what you try to accomplish, but this line:
request(correctPasscode, completionHandler: CompletionHandler)
doesn't compile because you are not passing a CompletionHandler closure to the function but rather a type object representing the type of that completion handler.
Hence the error: Cannot convert value of 'CompletionHandler.*Type*'.
A valid call would be:
request(correctPasscode) { result, error in
print("result was \(result), and error was \(error)")
}
But then your request function doesn't do anything with the closure which is passed in. It is a little hard to say what you want ...

Best way to handle errors from async closures in Swift 2?

I'm using a lot of async network request (btw any network request in iOS need to by async) and I'm finding way to better handle errors from Apple's dataTaskWithRequest which not supports throws.
I have code like that:
func sendRequest(someData: MyCustomClass?, completion: (response: NSData?) -> ()) {
let request = NSURLRequest(URL: NSURL(string: "http://google.com")!)
if someData == nil {
// throw my custom error
}
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) {
data, response, error in
// here I want to handle Apple's error
}
task.resume()
}
I need to parse my possible custom errors and handle possible connection errors from dataTaskWithRequest. Swift 2 introduced throws, but you can't throw from Apple's closure because they have no throw support and running async.
I see only way to add to my completion block NSError returning, but as I know using NSError is old-style Objective-C way. ErrorType can be used only with throws (afaik).
What's the best and most modern method to handle error when using Apple network closures? There is no way no use throws in any async network functions as I understand?
there are many ways you can solve this, but i would recommend using a completion block which expects a Result Enum. this would probably be the most 'Swift' way.
the result enum has exactly two states, success and error, which a big advantage to the usual two optional return values (data and error) which lead to 4 possible states.
enum Result<T> {
case Success(T)
case Error(String, Int)
}
Using the result enum in a completion block finishes the puzzle.
let InvalidURLCode = 999
let NoDataCode = 998
func getFrom(urlString: String, completion:Result<NSData> -> Void) {
// make sure the URL is valid, if not return custom error
guard let url = NSURL(string: urlString) else { return completion(.Error("Invalid URL", InvalidURLCode)) }
let request = NSURLRequest(URL: url)
NSURLSession.sharedSession().dataTaskWithRequest(request) { data, response, error in
// if error returned, extract message and code then pass as Result enum
guard error == nil else { return completion(.Error(error!.localizedDescription, error!.code)) }
// if no data is returned, return custom error
guard let data = data else { return completion(.Error("No data returned", NoDataCode)) }
// return success
completion(.Success(data))
}.resume()
}
because the return value is a enum, you should switch off of it.
getFrom("http://www.google.com") { result in
switch result {
case .Success(let data):
// handle successful data response here
let responseString = String(data:data, encoding: NSASCIIStringEncoding)
print("got data: \(responseString)");
case .Error(let msg, let code):
// handle error here
print("Error [\(code)]: \(msg)")
}
}
another solution would be to pass two completion blocks, one for success and one for error. something along the lines of:
func getFrom(urlString: String, successHandler:NSData -> Void, errorHandler:(String, Int) -> Void)
It's very similar to Casey's answer,
but with Swift 5, now we have Result (generic enumeration) implementation in standard library,
//Don't add this code to your project, this has already been implemented
//in standard library.
public enum Result<Success, Failure: Error> {
case success(Success), failure(Failure)
}
It's very easy to use,
URLSession.shared.dataTask(with: url) { (result: Result<(response: URLResponse, data: Data), Error>) in
switch result {
case let .success(success):
handleResponse(success.response, data: success.data)
case let .error(error):
handleError(error)
}
}
https://developer.apple.com/documentation/swift/result
https://github.com/apple/swift-evolution/blob/master/proposals/0235-add-result.md
There's an elegant approach utilising a JavaScript-like Promise library or a Scala-like "Future and Promise" library.
Using Scala-style futures and promises, it may look as follows:
Your original function
func sendRequest(someData: MyCustomClass?, completion: (response: NSData?) -> ())
may be implemented as shown below. It also shows, how to create a promise, return early with a failed future and how to fulfill/reject a promise:
func sendRequest(someData: MyCustomClass) -> Future<NSData> {
guard let url = ... else {
return Future.failure(MySessionError.InvalidURL) // bail out early with a completed future
}
let request = ... // setup request
let promise = Promise<NSData>()
NSURLSession.sharedSession().dataTaskWithRequest(request) { data, response, error in
guard let error = error else {
promise.reject(error) // Client error
}
// The following assertions should be true, unless error != nil
assert(data != nil)
assert(response != nil)
// We expect HTTP protocol:
guard let response = response! as NSHTTPURLResponse else {
promise.reject(MySessionError.ProtocolError) // signal that we expected HTTP.
}
// Check status code:
guard myValidStatusCodeArray.contains(response.statusCode) else {
let message: String? = ... // convert the response data to a string, if any and if possible
promise.reject(MySessionError.InvalidStatusCode(statusCode: response.statusCode, message: message ?? ""))
}
// Check MIME type if given:
if let mimeType = response.MIMEType {
guard myValidMIMETypesArray.contains(mimeType) else {
promise.reject(MySessionError.MIMETypeNotAccepted(mimeType: mimeType))
}
} else {
// If we require a MIMEType - reject the promise.
}
// transform data to some other object if desired, can be done in a later, too.
promise.fulfill(data!)
}.resume()
return promise.future!
}
You might expect a JSON as response - if the request succeeds.
Now, you could use it as follows:
sendRequest(myObject).map { data in
return try NSJSONSerialization.dataWithJSONObject(data, options: [])
}
.map { object in
// the object returned from the step above, unless it failed.
// Now, "process" the object:
...
// You may throw an error if something goes wrong:
if failed {
throw MyError.Failed
}
}
.onFailure { error in
// We reach here IFF an error occurred in any of the
// previous tasks.
// error is of type ErrorType.
print("Error: \(error)")
}