Ezak Ezak - 1 month ago 10
Swift Question

Swift POST Request in same Thread

Hope you can help me. I want a swift function that make a post request and return the json data

so here is my class

import Foundation


class APICall {

//The main Url for the api
var mainApiUrl = "http://url.de/api/"

func login(username: String, password: String) -> String {
let post = "user=\(username)&password=\(password)";
let action = "login.php";
let ret = getJSONForPOSTRequest(action: action, post: post)
return ret;
}

//Function to call a api and return the json output
func getJSONForPOSTRequest(action: String, post: String) -> String {
var ret: String?

let apiUrl = mainApiUrl + action;

let myUrl = URL(string: apiUrl);
var request = URLRequest(url:myUrl!);
request.httpMethod = "POST";

let postString = post;

request.httpBody = postString.data(using: String.Encoding.utf8);

let task = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in

if error != nil
{
print("error=\(error)")
return
}

print("response=\(response)")

do {
let json = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? NSDictionary

if let parseJSON = json {
let login = parseJSON["Login"] as? String
print("login: \(login)")
ret = login
}
} catch {
print(error)
}
}
task.resume()
return ret!;
}

}


But ret is nil. In the debugger is see the inner of the task is called later by another thread?

How can if fix that?

Thank you guys

Answer

The data task completion closure is called on another thread and after the execution of the method is completed so you need to re-jig your code a bit. Instead of having a String return value for your getJSONForPOSTRequest, don't return anything and instead have an additional argument that is a closure and call that from within your dataTask closure instead.

func getJSONForPOSTRequest(action: String, post: String, completion: (string: String) -> Void) {

    // ...

    let task = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in

        // ... (Convert data to string etc.)

        completion(string: myString)
    }

    task.resume()
}

Remember, doing this means that the completion handler will be called once the network request completes and not right away.

EDIT:

Lets take this from the beginning. When you download something from the network in iOS you typically use NSURLSession. NSURLSession has a number of methods available to it for different means of interacting with the network, but all of these methods use a different thread, typically a background thread, which will do work independently of the rest of your code.

With this in mind, when you call the dataTask method you will notice that you have to add a completion closure as one of the parameters (notice in your example you are using something called a 'trailing closure' which is a closure that is the last argument in the method call that doesn't fall within the parenthesis of the method with the rest of the arguments). Think of a closure as a piece of code that is executed at a different time, it's not executed in line with the rest of the code around it (See the Swift documentation on closures here). In this case the closure will be called once the network request has been completed. Network requests aren't instant so we typically use a background thread to execute them while the user is shown an activity indicator etc and can still use the app. If we waited until the network request completed on the same thread as the rest of our code then it results in the app appearing laggy and even frozen which is terrible for users.

So going back to your example at hand; when you call your getJSONForPOSTRequest method the code within that method will complete and return before the network request has completed which is why we don't need to use a return value. Once the network request has completed your closure code will get called. Because the closure is called later it's also being called from an entirely different place within the code, in this case it's called from within iOS's network code. Because if this if you return a value from within the closure you will be trying to return the value to the network code which isn't what you want, you want to return the value to your own code.

To return the value of the network response to your code you need to define a closure (or a delegate, but I'm not going to go into that here) yourself. If you look at the example code above I've removed the return value from your getJSONForPOSTRequest method and added a new argument called 'completion', and if you look at the type of that argument you can see it's (string: String) -> Void, this defines a closure that passes in a string (the string that you will have downloaded from the network). Now that we have a closure thats within your method we can use this to call back to the caller of the getJSONForPOSTRequest with the data we have downloaded form the network.

Lets take your login method and see how we use getJSONForPOSTRequest within it:

func login(username: String, password: String, completion: (success: Bool) -> Void) {

    let post = "user=\(username)&password=\(password)";
    let action = "login.php";

    let ret = getJSONForPOSTRequest(action: action, post: post) { string in 

        // This will be called once the network has responded and 'getJSONForPOSTRequest' has processed the data
        print(string)

        completion(success: true)
    }
}

See that again we aren't returning anything directly from the login method as it has to rely on the a-synchronousness of calling off to the network.

It might feel by now that you are starting to get into something called 'callback hell', but this is the standard way to deal with networking. In your UI code you will call login and that will be the end of the chain. For example here is some hypothetical UI code:

func performLogin() {

    self.activityIndicator.startAnimating()

    self.apiCaller.login(username: "Joe", password: "abc123") { [weak self] success in 

        print(success)

        // This will get called once the login request has completed. The login might have succeeded of failed, but here you can make the decision to show the user some indication of that
        self?.activityIndicator.stopAnimating()
        self?.loginCompleted()
    }
}

Hopefully that clarifies a few things, if you have any other questions just ask.

Comments