Swift http request use urlSession - swift

I want to write func for HTTP Request to my server and get some data, when i print it (print(responseString)) it looks good, but when i try to return data, its always empty
public func HTTPRequest(dir: String, param: [String:String]?) -> String{
var urlString = HOST + dir + "?"
var responseString = ""
if param != nil{
for currentParam in param!{
urlString += currentParam.key + "=" + currentParam.value + "&"
}
}
let url = URL(string: urlString)
let task = URLSession.shared.dataTask(with: url!) { data, response, error in
guard error == nil else {
print("ERROR: HTTP REQUEST ERROR!")
return
}
guard let data = data else {
print("ERROR: Empty data!")
return
}
responseString = NSString(data: data,encoding: String.Encoding.utf8.rawValue) as! String
print(responseString)
}
task.resume()
return responseString
}

As mentioned in Rob's comments, the dataTask closure is run asynchronously. Instead of returning the value immediately, you would want to provide a completion closure and then call it when dataTask completes.
Here is an example (for testing, can be pasted to Xcode Playground as-is):
import Foundation
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
let HOST = "http://example.org"
public func HTTPRequest(dir: String, param: [String: String]?, completion: #escaping (String) -> Void) {
var urlString = HOST + dir + "?"
if param != nil{
for currentParam in param! {
urlString += currentParam.key + "=" + currentParam.value + "&"
}
}
let url = URL(string: urlString)
let task = URLSession.shared.dataTask(with: url!) { data, response, error in
guard error == nil else {
print("ERROR: HTTP REQUEST ERROR!")
return
}
guard let data = data else {
print("ERROR: Empty data!")
return
}
let responseString = NSString(data: data,encoding: String.Encoding.utf8.rawValue) as! String
completion(responseString)
}
task.resume()
}
let completion: (String) -> Void = { responseString in
print(responseString)
}
HTTPRequest(dir: "", param: nil, completion: completion)

You need to use completion block instead of returning value because the dataTask closure is run asynchronously, i.e. later, well after you return from your method. You don't want to try to return the value immediately (because you won't have it yet). You want to (a) change this function to not return anything, but (b) supply a completion handler closure, which you will call inside the dataTask closure, where you build responseString.
For example, you might define it like so:
public func HTTPRequest(dir: String, param: [String:String]? = nil, completionHandler: #escaping (String?, Error?) -> Void) {
var urlString = HOST + dir
if let param = param {
let parameters = param.map { return $0.key.percentEscaped() + "=" + $0.value.percentEscaped() }
urlString += "?" + parameters.joined(separator: "&")
}
let url = URL(string: urlString)
let task = URLSession.shared.dataTask(with: url!) { data, response, error in
guard let data = data, error == nil else {
completionHandler(nil, error)
return
}
let responseString = String(data: data, encoding: .utf8)
completionHandler(responseString, nil)
}
task.resume()
}
Note, I'm percent escaping the values in the parameters dictionary using something like:
extension String {
/// Percent escapes values to be added to a URL query as specified in RFC 3986
///
/// This percent-escapes all characters besides the alphanumeric character set and "-", ".", "_", and "~".
///
/// http://www.ietf.org/rfc/rfc3986.txt
///
/// - Returns: Returns percent-escaped string.
func percentEscaped() -> String {
let allowedCharacters = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~")
return self.addingPercentEncoding(withAllowedCharacters: allowedCharacters)!
}
}
And then you'd call it like so:
HTTPRequest(dir: directory, param: parameterDictionary) { responseString, error in
guard let responseString = responseString else {
// handle the error here
print("error: \(error)")
return
}
// use `responseString` here
DispatchQueue.main.async {
// because this is called on background thread, if updating
// UI, make sure to dispatch that back to the main queue.
}
}
// but don't try to use `responseString` here

Related

How to write a completion handler in a separated block of code in Swift with parameters out of scope

I was trying to make my code clean and decouple the code below, I want to remove the trailing completion handler from it and write the completion handler in another blck of code.
func uploadMarcasMetodoNovo(_ id_resenha: Int) {
let resenhaDados:ResDadoModel = db.readDadosResenhaById(id_resenha)
let resenhaMarcas:[ResMarcasModel] = db.readResMarca(id_resenha)
// this for loop runs about for 7 times
for marca in resenhaMarcas {
contadorUploadMarcas = contadorUploadMarcas + 1
myUploadGroupMarcas.enter()
jsonRequestUploadImagemGrafica = ResMarcasModel.createJsonMarcaResenha(marca, resenhaDados.IdGedave )
let json: [String: Any] = jsonRequestUploadImagemGrafica
guard let jsonData = try? JSONSerialization.data(withJSONObject: json) else {
print("guard jsonData error")
return
}
let requestImagemGrafica = requestUploadFotos(jsonData)
let task = URLSession.shared.dataTask(with: requestImagemGrafica) { data, response, error in
if let error = error {
print("error: \(String(describing: error))")
return
}
print("data")
guard let returnData = String(data: data!, encoding: .utf8) else {
print("returnData guard fail")
return
}
print("returnData")
print(returnData)
self.confirmStatusEnviada(marca)
self.myUploadGroupMarcas.leave()
print("end TASK")
}
task.resume()
}
myUploadGroupMarcas.notify(queue: DispatchQueue.main) {
print("myUploadGroupMarcas notify")
// more code ...
}
}
This is the part that I write creating a separated completion handler
let myCompletionHandler: (Data?, URLResponse?, Error?) -> Void = {
(data, response, error) in
if let error = error {
print("error: \(String(describing: error))")
return
}
print("data")
guard let returnData = String(data: data!, encoding: .utf8) else {
print("returnData guard fail")
return
}
self.confirmStatusEnviada(marca)
self.myUploadGroupMarcas.leave()
}
but it won't work because in the last two lines of code are used paramters that are out of scope. The parameter "marca" and the parameter "myUploadGroupMarcas" are out of scope. Is there a way to use these parameters inside the separated completion handler function?
Ok based on our comments above, this is the route I would try: Write a short completion handler that calls your longer completion handler, passing the variables that are out of scope.
let task = URLSession.shared.dataTask(with: requestImagemGrafica) { data, response, error in
myCompletionHandler(data, response, error, marca, myUploadGroupMarcas)
}
Then you add two parameters to your completion handler in the function definition:
let myCompletionHandler: (Data?, URLResponse?, Error?, MarcaClass, myUploadGroupMarcas) -> Void
Obviously you need to replace MarcaClass with the actual class type that is marca and myUploadGroupMarcas seems to be a function so you'd need to write an appropriate parameter type for that.

How do I set a the serverResponse var from within the if let data string statement

func getResponse(serverName: String) -> String {
var serverResponse: String = "No Response"
let serverURL = "http://" + serverName + ":3000"
if let url = URL(string: serverURL) {
URLSession.shared.dataTask(with: url) { data, response, error in
if let data = data {
if let jsonString = String(data: data, encoding: .utf8) {
serverResponse = jsonString
print(jsonString)
}
}
}.resume()
}
return serverResponse
}
I'm trying to set the serverResponse variable from within the if let jsonString but it always returns "No response"(the vars default) and the print function from within the if let jsonString will print out the server response.
DataTask is asynchronous. your function is returning the value before the server request has been completed. You should use a completion handler here.
func getResponse(serverName: String , completion : #escaping (Bool,String?) -> ()) {
let serverURL = "http://" + serverName + ":3000"
if let url = URL(string: serverURL) {
URLSession.shared.dataTask(with: url) { data, response, error in
if let data = data {
if let jsonString = String(data: data, encoding: .utf8) {
print(jsonString)
completion(true,jsonString)
}
} else {
completion(false,nil)
}
}.resume()
}
}
Then you can call the above function like this:
getResponse(serverName: "yourServerName") { (isSuccess, response) in
if isSuccess {
self.serverResponse = response ?? ""
} else {
// your api request failed. show alert or whatever you want to inform the user.
}
}
You need to add a completionHandler.
func getResponse(serverName: String, onCompletion: #escaping (String?) -> Void) {
var serverResponse: String = "No Response"
let serverURL = "http://" + serverName + ":3000"
if let url = URL(string: serverURL) {
URLSession.shared.dataTask(with: url) { data, response, error in
if let data = data {
if let jsonString = String(data: data, encoding: .utf8) {
serverResponse = jsonString
print(jsonString)
onCompletion(serverResponse)
}
}
}.resume()
}
}
Create serverResponse outside the getResponse(serverName:) method and use property observer didSet to observe the changes in serverResponse, i.e.
var serverResponse: String = "No Response" {
didSet {
print("newValue: ", serverResponse)
//add the code here..
}
}
didSet will be called every time there is any change in serverResponse. So, any code that you want to run after getting the serverResponse from API, write here.
Also, no need to return anything from getResponse(serverName:) method. So, the method will now look like,
func getResponse(serverName: String) {
let serverURL = "http://" + serverName + ":3000"
if let url = URL(string: serverURL) {
URLSession.shared.dataTask(with: url) { data, response, error in
if let data = data {
if let jsonString = String(data: data, encoding: .utf8) {
serverResponse = jsonString
print(jsonString)
}
}
}.resume()
}
}

Swift function produces a "SIGILL" on it's return statement

I am writing a piece of code in Swift to hit a public API endpoint to pull back data in JSON and use it in the application. I am using URLSession to do the request and am using an async/await similar paradigm to extract data out of the URLSession callback and place it in a local variable. Then, the function returns the optional dictionary returned by JSONSerialization to the caller.
This code executes perfectly fine outside of a function and run as part of the main program, but as soon as it is moved to a function, the return statement produces a "SIGILL" exit.
I breakpointed to the return statement and found that it is exactly what is throwing this error. Since this is an optional dictionary, I tried just returning an unwrapped version of the dictionary and found the same results. I also tried just returning a blank dictionary and I still get a SIGILL
Functioning:
let url = URL(string: <endpointURL>)!
var tenant: [String: Any]? = nil;
let sem = DispatchSemaphore(value: 1)
sem.wait()
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if let data = data {
do {
let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]
if let json = json {
print(json)
tenant = json
sem.signal()
} else {
print("ERR: Null JSON")
}
} catch let error as NSError {
print("ERR: " + error.localizedDescription)
}
} else if let error = error {
print("ERR: " + error.localizedDescription);
} else {
print("ERR: Unknown")
}
}
print("resuming")
task.resume()
print("waiting: ")
sem.wait()
print("done waiting")
print(tenant!["tenant_name"]!)
Fails:
let _ = HttpHelper.getTenantFor(tenantId: <someUUID>)
class HttpHelper {
static func getTenantFor(tenantId: String) -> [String:Any]? {
let url = URL(string: <endpointURL>)!
var tenant: [String: Any]? = nil;
let sem = DispatchSemaphore(value: 1)
sem.wait()
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if let data = data {
do {
let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]
if let json = json {
print(json)
tenant = json
sem.signal()
} else {
print("ERR: Null JSON")
}
} catch let error as NSError {
print("ERR: " + error.localizedDescription)
}
} else if let error = error {
print("ERR: " + error.localizedDescription);
} else {
print("ERR: Unknown")
}
}
print("resuming")
task.resume()
print("waiting: ")
sem.wait()
print("done waiting")
return [String:Any]()
}
}
On the functioning code, the app outputs the proper value for the "tenant_name" key in the JSON object and in the failed code I get the following:
Process finished with exit code 132 (interrupted by signal 4: SIGILL)

Returning data from async call that takes multiple params in Swift function

I was trying to create a post method so I could reuse it further in my code.
I saw this example Returning data from async call in Swift function that gives partial solution to my problem but don't know how to call the function once I define it.
This is the function I am trying to call:
class func postRequest(url: URL, request: URLRequest, saveCookie: Bool, completionHandler: #escaping (_ postRequestStatus: [String:Any]) -> ()) {
let session = URLSession.shared
//So now no need of type conversion
let task = session.dataTask(with: request) {
(data, response, error) in
func displayError(_ error: String) {
print(error)
}
/* GUARD: Was there an error? */
guard (error == nil) else {
displayError("There was an error with your request: \(String(describing: error))")
return
}
guard let statusCode = (response as? HTTPURLResponse)?.statusCode, statusCode >= 200 && statusCode <= 299 else {
displayError("Your request returned a status code other than 2xx!")
return
}
/* GUARD: Was there any data returned? */
guard let data = data else {
displayError("No data was returned by the request!")
return
}
/* Since the incoming cookies will be stored in one of the header fields in the HTTP Response,parse through the header fields to find the cookie field and save the data */
if saveCookie{
let httpResponse: HTTPURLResponse = response as! HTTPURLResponse
let cookies = HTTPCookie.cookies(withResponseHeaderFields: httpResponse.allHeaderFields as! [String : String], for: (response?.url!)!)
HTTPCookieStorage.shared.setCookies(cookies as [AnyObject] as! [HTTPCookie], for: response?.url!, mainDocumentURL: nil)
}
let json: [String:Any]?
do
{
json = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String:Any] ?? [:]
}
catch
{
displayError("Could not parse the data as JSON: '\(data)'")
return
}
guard let server_response = json else
{
displayError("Could not parse the data as JSON: '\(data)'")
return
}
if let userID = server_response["UserID"] as? Int64 {
print(userID)
completionHandler(server_response)
}else{
displayError("Username or password incorrect.")
}
}
return task.resume()
}
This is the caller function:
class func loginPostRequest(post_data: [String:Any], completionHandler: #escaping (_ postRequestStatus: [String:Any]) -> ()){
let url = URL(string: HTTPConstant.Login.Url)!
var request = URLRequest(url: url)
request.httpMethod = "POST"
var paramString = ""
for (key, value) in post_data
{
paramString = paramString + (key) + "=" + (value as! String) + "&"
}
request.httpBody = paramString.data(using: .utf8)
//in the line below I get the error message, extra argument "request" in call.
postRequest(url: url, request: request, saveCookie: true, completionHandler: { postRequestStatus in
completionHandler(postRequestStatus)
})
}
You cannot make loginPostRequest return NSDictionary because you are making async call with what you need is to create completion block same way you have create with postRequest method also from Swift 3 you need to use URLRequest with mutable var object instead of NSMutableURLRequest you need to also change the postRequest function's request argument type to URLRequest so latter no need to convert NSMutableURLRequest to URLRequest and use Swift type dictionary instead of NSDictionary
class func loginPostRequest(post_data: [String:Any], completionHandler: #escaping (_ postRequestStatus: [String:Any]) -> ()){
let url = URL(string: HTTPConstant.Login.Url)!
var request = URLRequest(url: url)
request.httpMethod = "POST"
var paramString = ""
for (key, value) in post_data
{
paramString = paramString + (key as! String) + "=" + (value as! String) + "&"
}
request.httpBody = paramString.data(using: .utf8)
postRequest(url: url, request: request, saveCookie: true, completionHandler: { postRequestStatus in
completionHandler(postRequestStatus)
})
}
Now simply changed the argument type of request to URLRequest from NSMutableURLRequest in method postRequest
class func postRequest(url: URL, request: URLRequest, saveCookie: Bool, completionHandler: #escaping (_ postRequestStatus: [String:Any]) -> ()) {
let session = URLSession.shared
//So now no need of type conversion
let task = session.dataTask(with: request) { (data, response, error) in
func displayError(_ error: String) {
print(error)
}
/* GUARD: Was there an error? */
guard (error == nil) else {
displayError("There was an error with your request: \(String(describing: error))")
return
}
guard let statusCode = (response as? HTTPURLResponse)?.statusCode, statusCode >= 200 && statusCode <= 299 else {
displayError("Your request returned a status code other than 2xx!")
return
}
/* GUARD: Was there any data returned? */
guard let data = data else {
displayError("No data was returned by the request!")
return
}
/* Since the incoming cookies will be stored in one of the header fields in the HTTP Response,parse through the header fields to find the cookie field and save the data */
if saveCookie{
let httpResponse: HTTPURLResponse = response as! HTTPURLResponse
let cookies = HTTPCookie.cookies(withResponseHeaderFields: httpResponse.allHeaderFields as! [String : String], for: (response?.url!)!)
HTTPCookieStorage.shared.setCookies(cookies as [AnyObject] as! [HTTPCookie], for: response?.url!, mainDocumentURL: nil)
}
let json: [String:Any]?
do
{
json = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String:Any] ?? [:]
}
catch
{
displayError("Could not parse the data as JSON: '\(data)'")
return
}
guard let server_response = json else
{
displayError("Could not parse the data as JSON: '\(data)'")
return
}
if let userID = server_response["UserID"] as? Int64 {
print(userID)
completionHandler(server_response)
}else{
displayError("Username or password incorrect.")
}
}
return task.resume()
}
Now when you call this loginPostRequest you are having response in completion block of it.
Functions that receive a closure as parameter can be called like any other functions:
postRequest(url: yourUrlObject, request: yourUrlRequest, saveCookie: true/false, completionHandler: { postRequestStatus in
// ... code that will run once the request is done
})
If the closure is the last parameter you can pass it outside the parenthesis:
postRequest(url: yourUrlObject, request: yourUrlRequest, saveCookie: true/false) { postRequestStatus in
// ... code that will run once the request is done
})
You can check the Swift book to learn more about closures and functions.
By the way, your postRequest method looks weird, I haven't checked deeply into it, but for instance I believe although url is one of the parameters it isn't actually used. Some other answer pointed other problems into that function.

JSON is not convertible to void (Openweather map API)

I am calling Openweather map API using Swift and from the response I need to return a particular value as string.
However when I try to return the value error comes as JSON is not convertible to string.
func callWeatherServ(name:String, completion:(Dictionary<String,AnyObject>) -> Void)
{
var baseUrl: String = "http://api.openweathermap.org/data/2.5/weather"
var url: String = "\(baseUrl)?q=\(name)"
let finalUrl: NSURL = NSURL(string: url)!
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithURL(finalUrl, completionHandler: {data, response, error -> Void in
if error != nil
{
// If there is an error in the web request, print it to the console
println(error.localizedDescription)
}
var err: NSError?
var jsonResult = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: &err) as! NSDictionary
if err != nil
{
// If there is an error parsing JSON, print it to the console
println("JSON Error \(err!.localizedDescription)")
}
let json = JSON(jsonResult)
println("response is \(json) ")
var weathername = json["weather"][0]["main"]
if (weathername != nil)
{
return weathername
}
})
task.resume()
}
I get that since we have used closure whose return type void so we should use completion handler. But I am not aware how we can do that.
Also how we can call the function if we pass completion handler as parameter?
If you want to keep using SwiftyJSON as in your example, here's how to do it:
change the type of the completion handler from a dictionary to the JSON type used by SwiftyJSON.
then wrap the value you want to "return" in the handler.
then call your method as in my example, with a trailing closure
Swift 2
func callWeatherServ(name:String, completion:(object: JSON) -> Void) {
let baseUrl: String = "http://api.openweathermap.org/data/2.5/weather"
let url: String = "\(baseUrl)?q=\(name)"
if let finalUrl = NSURL(string: url) {
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithURL(finalUrl, completionHandler: {data, response, error -> Void in
if let error = error {
print(error.localizedDescription)
} else {
if let data = data {
let json = JSON(data: data)
print("response is \(json) ")
completion(object: json["weather"][0]["main"])
} else {
print("No data")
}
}
})
task.resume()
}
}
Call the method:
callWeatherServ("paris") { (object) in
// here you get back your JSON object
print(object)
}
Note that you were parsing your data twice, with NSJSONSerialization and with SwiftyJSON, so I've removed the unnecessary NSJSONSerialization part.
Original Swift 1 version
func callWeatherServ(name:String, completion:(object: JSON) -> Void)
{
var baseUrl: String = "http://api.openweathermap.org/data/2.5/weather"
var url: String = "\(baseUrl)?q=\(name)"
let finalUrl: NSURL = NSURL(string: url)!
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithURL(finalUrl, completionHandler: {data, response, error -> Void in
if error != nil
{
// If there is an error in the web request, print it to the console
println(error.localizedDescription)
}
var err: NSError?
let json = JSON(data: data, options: NSJSONReadingOptions.allZeros, error: &err)
println("response is \(json) ")
var weathername = json["weather"][0]["main"]
if (weathername != nil)
{
completion(object: weathername)
}
})
task.resume()
}
Call the method:
callWeatherServ("paris", completion: { (object) -> Void in
println(object) // "Clear"
})
Implement completion handler from where you are calling this method and use the string at that place only no need to return the string.
You can directly use it from the completion handle by implemet it in caller function