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
Related
I got this error when I want to send value with GET method:
Thread 1: EXC_BREAKPOINT (code=1, subcode=0x10136bb50)
To get values:
var flname = self.txt_field_flname.text
var job_title = self.txt_field_job_title.text
var mobile = self.txt_field_mobile.text
var des = self.txt_field_des.text
var lat = self.lat
var lon = self.lon
self.sendNewJob(fname: flname!, title: job_title!, mobile: mobile!, des: des!, lat: String(lat), lon: String(lon) )
func sendNewJob(fname:String,title:String,mobile:String,des:String,
lat:String,lon:String)
{
print("fname \(fname) title \(title) mobile \(mobile) des \(des) lat \(lat) lon \(lon)") //output is well
RestApiManager.sharedInstance.sendNewJob(fname: fname,title: title,mobile:mobile,
des:des,lat:lat,lon:lon) { (json: JSON) in
}
}
func sendNewJob(fname:String,title:String,mobile:String,des:String,
lat:String,lon:String,onCompletion: #escaping (JSON) -> Void) {
let route = baseURL+"up=1&Name=\(fname)&BusinessName=\(title)&MobileNumber=\(mobile)&latitude=\(lat)&longitude=\(lon)&Description=\(des)"
makeHTTPGetRequest(path: route, onCompletion: { json, err in
onCompletion(json as JSON)
})
}
// MARK: Perform a GET Request
private func makeHTTPGetRequest(path: String, onCompletion: #escaping ServiceResponse) {
let request = NSMutableURLRequest(url: NSURL(string: path)! as URL) // line of my error
let session = URLSession.shared
let task = session.dataTask(with: request as URLRequest, completionHandler: {data, response, error -> Void in
if let jsonData = data {
let json:JSON = JSON(data: jsonData)
onCompletion(json, error as NSError?)
} else {
onCompletion(nil, error as NSError?)
}
})
task.resume()
}
This happens when the code executes a nil value. Here the code NSURL(string: path)! value might be nil. You can use optional binding (if let) to check whether the NSURL is a valid one. It happens when the string is not valid and does not make a valid URL.
You can use like this :
private func makeHTTPGetRequest(path: String, onCompletion: #escaping ServiceResponse) {
if let encodedPath = path.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) {
if let url = URL(string: encodedPath) {
let request = URLRequest(url: url)
let session = URLSession.shared
let task = session.dataTask(with: request as URLRequest, completionHandler: {data, response, error -> Void in
if let jsonData = data {
let json:JSON = JSON(data: jsonData)
onCompletion(json, error as NSError?)
} else {
onCompletion(nil, error as NSError?)
}
})
task.resume()
} else {
print("url is nil")
onCompletion(nil)
}
} else {
print("unable to encode url")
onCompletion(nil)
}
}
I have this code :
let task = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in
if error != nil {
print(error!)
return
}
do {
let json = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? NSDictionary
if let parseJSON = json {
let getDetail = parseJSON["detail"] as? String
returnDetail = getDetail!.base64Decoded()
} // parse json end
} // do end
catch {
print(error)
}
} // let task end
returnDetail has been defined previously. I did anything to set returnDetail value to getDetail!.base64Decoded() but it only works inside let task = ...
How can I pass it to the outer scope?
You have several methods to tackle the issue of returning a value from inside an asynchronous function. One of them is to wrap the asynchronous network call inside a function and make it return a completionHandler.
Some general advice: don't use force unwrapping unless you are 100% sure that your optional value won't be nil. With network requests, the data can be nil even if there's no error, so never force unwrap data, use safe unwrapping with if let or guard let. Don't use .mutableContainers in Swift when parsing a JSON value, since it has no effect. The mutability of the parsed JSON object is decided by using the let or var keyword to declare the variable holding it. Also don't use NSDictionary, use its native Swift counterpart, Dictionary ([String:Any] is a shorthand for the type Dictionary<String,Any>).
func getDetail(withRequest request: URLRequest, withCompletion completion: #escaping (String?, Error?) -> Void) {
let task = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in
if error != nil {
completion(nil, error)
return
}
else if let data = data {
do {
guard let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String:Any] else {completion(nil, nil);return}
guard let details = json["detail"] as? String else {completion(nil, nil);return}
completion(details, nil)
}
catch {
completion(nil, error)
}
}
}
task.resume()
}
Then you can call this function by
getDetail(withRequest: request, withCompletion: { detail, error in
if error != nil {
//handle error
} else if detail = detail {
//You can use detail here
}
})
I would suggest to use a completion handler.
func foo(withCompletion completion: (String?, Error?) -> Void) {
let task = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in
if error != nil {
completion(nil, error)
return
}
do {
let json = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? NSDictionary
if let parseJSON = json {
let details = parseJSON["detail"] as? String
completion(details, nil)
} // parse json end
} // do end
catch {
completion(nil, error)
}
} // let task end
}
I think, you use CallBack(Clourse) of Swift to return data when getDetail have data.
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.
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
please spare me. Im new to swift
my problem was I cant return my NSDictionary this is my function
private func request(url:String, baseURL:String) -> NSDictionary {
var dict:NSDictionary!
var request = HTTPTask()
request.requestSerializer = HTTPRequestSerializer()
request.requestSerializer.headers[headerKey] = getToken() //example of adding a header value
request.baseURL = baseURL
request.GET(url, parameters: nil, success: {(response: HTTPResponse) in
if var data = response.responseObject as? NSData {
let str = NSString(data: data, encoding: NSUTF8StringEncoding)
var error: NSError?
dict = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: &error) as NSDictionary
println("response: \(dict)")
}
},failure: {(error: NSError, response: HTTPResponse?) in
println("error: \(error)")
})
return dict
}
the return dict is just empty like {} but when i println("response: \(dict)") inside the function seem to log my data.
I think my function return an empty object because the .GET method is running in different thread and waiting for a response.
Could anyone help me, any comment would do.
You are right, request runs asynchronously in another thread, I would suggest using completion handler.
private func request(url: String, baseURL: String, completion: (result: NSDictionary) -> Void) {
var dict:NSDictionary!
var request = HTTPTask()
request.requestSerializer = HTTPRequestSerializer()
request.requestSerializer.headers[headerKey] = getToken() //example of adding a header value
request.baseURL = baseURL
request.GET(url, parameters: nil, success: {(response: HTTPResponse) in
if var data = response.responseObject as? NSData {
let str = NSString(data: data, encoding: NSUTF8StringEncoding)
var error: NSError?
dict = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: &error) as NSDictionary
println("response: \(dict)")
completion(result: dict)
}
},failure: {(error: NSError, response: HTTPResponse?) in
println("error: \(error)")
completion(result: nil) //this is not the best option, better would be to return error in error handler
})
}
For more info check out this: http://www.veasoftware.com/tutorials/2015/1/13/completion-handlers-swift-programming-tutorial