Swift calling completion handler in from another file fails - swift

I am calling a funciton with completio=n handler from one calss to another class
Called class:
class PVClass
{
var avgMonthlyAcKw:Double = 0.0
var jsonString:String!
func estimateMonthlyACkW (areaSqFt:Float, completion: #escaping(Double) -> () ){
var capacityStr:String = ""
let estimatedCapacity = Float(areaSqFt/66.0)
capacityStr = String(format: "%.2f", estimatedCapacity)
// Build some Url string
var urlString:String = "https://developer.nrel.gov/"
urlString.append("&system_capacity=")
urlString.append(capacityStr)
let pvURL = URL(string: urlString)
let dataTask = URLSession.shared.dataTask(with: pvURL!) { data, response, error in
do {
let _ = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers)
self.jsonString = String(data: data!, encoding: .utf8)!
print("JSON String:\(String(describing: self.jsonString))")
if self.jsonString != nil {
let decoder = JSONDecoder()
let jsonData = try decoder.decode(PVClass.Top.self, from: data!)
// do some parsing here
var totalAcKw: Double = 0.0
let cnt2: Int = (jsonData.Outputs?.ACMonthly.count)!
for i in 0..<(cnt2-1) {
totalAcKw = totalAcKw + (jsonData.Outputs?.ACMonthly[i])!
}
self.avgMonthlyAcKw = Double(totalAcKw)/Double(cnt2)
// prints value
print("updated estimate: ", self.avgMonthlyAcKw)
}
} catch {
print("error: \(error.localizedDescription)")
}
}
dataTask.resume()
completion(self.avgMonthlyAcKw)
}
Calling Class:
func estimate() {
var estimatedSolarkWh:Double = 0.0
let aPVClass = PVClass()
aPVClass.estimateMonthlyACkW(areaSqFt: 100.0, completion: { (monthlyAckW) -> Void in
estimatedSolarkWh = monthlyAckW
self.view.setNeedsDisplay()
})
return
}
}
When I call the function estimate() the estimateMonthlyACkW function in the other PVClass is executed but it returns after the calling estimate() function is executed. So even though in the called function the URLsession is executed, json is parsed, and value is printed correctly - the value never gets gets transferred to the completion handler and the value never comes back to calling class. How can I fix this?

You need to move completion(self.avgMonthlyAcKw) just after print statement like below:
// prints value
print("updated estimate: ", self.avgMonthlyAcKw)
completion(self.avgMonthlyAcKw)
Hope this will helps you :)

Related

How can I extract jsonString from this method that depends on a task?

var temp = ""
let appid = "**************"
struct WeatherData {
// Object with latitude and longitude to process requests
// from OpenWeatherMap.
var lat, lon: Float
init(latitude: Float, longitude: Float) {
lat = latitude
lon = longitude
}
func retrieve() {
var jsonString = ""
// Send request to OpenWeatherMap.
let requestAddress = "https://api.openweathermap.org/data/2.5/weather?lat=\(lat)&lon=\(lon)&appid=\(appid)"
// Assign the URL to retrieve JSON, with ! dangerous
// operation.
let url = URL(string: requestAddress)!
let urlSession = URLSession(configuration: .ephemeral)
let task = urlSession.dataTask(with: url) {(data, response, error) in
let data = data
jsonString = String(data: data!, encoding: .utf8)!
print(jsonString)
}
task.resume()
}
}
var bangkok = WeatherData(latitude: 13.736717, longitude: 100.523186)
print(bangkok.retrieve())
print("Program running...")
print(temp)
RunLoop.main.run()
The issue I'm having is only print() works but not a return statement or an assignment to a global variable which is what I need. I ultimately want to parse this jsonString into a working dictionary except that I can't get it out of the function at the moment.
I'm depending on a webpage that has only text as body content.
It common to return the result via a completion block:
enum AppError : String, Error
{
case unknownFailure
case requestFailed
...
}
func retrieve(completion: #escaping (Result<Data, AppError>) -> Void)
{
...
let task = urlSession.dataTask(with: url)
{ (data, response, error) in
DispatchQueue.main.async
{
if let statusCode = (response as? HTTPURLResponse)?.statusCode,
statusCode != 200
{
completion(.failure(.requestFailed))
}
else if let data = data
{
completion(.success(data))
}
else if let error = error
{
completion(.failure(.requestFailed))
}
else
{
completion(.failure(.unknownFailure))
}
}
}
task.resume()
}
You need to choose where to convert the received Data to JSON and ultimately to some Codable struct.
You must also decide on how to handle errors and what detail you want to pass to a caller. In the above example I hide the details of dataTask() errors and put them all under app-custom .requestFailed.

return value from completion handler is not updated in DispatchQueue.main.async block

I am calling a function with completion handler from one class to another class
called class:
class PVClass
{
var avgMonthlyAcKw:Double = 0.0
var jsonString:String!
func estimateMonthlyACkW (areaSqFt:Float, completion: #escaping(Double) -> () ){
var capacityStr:String = ""
let estimatedCapacity = Float(areaSqFt/66.0)
capacityStr = String(format: "%.2f", estimatedCapacity)
// Build some Url string
var urlString:String = "https://developer.nrel.gov/"
urlString.append("&system_capacity=")
urlString.append(capacityStr)
let pvURL = URL(string: urlString)
let dataTask = URLSession.shared.dataTask(with: pvURL!) { data, response, error in
do {
let _ = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers)
self.jsonString = String(data: data!, encoding: .utf8)!
print("JSON String:\(String(describing: self.jsonString))")
if self.jsonString != nil {
let decoder = JSONDecoder()
let jsonData = try decoder.decode(PVClass.Top.self, from: data!)
// do some parsing here
var totalAcKw: Double = 0.0
let cnt2: Int = (jsonData.Outputs?.ACMonthly.count)!
for i in 0..<(cnt2-1) {
totalAcKw = totalAcKw + (jsonData.Outputs?.ACMonthly[i])!
}
self.avgMonthlyAcKw = Double(totalAcKw)/Double(cnt2)
// prints value
print("updated estimate: ", self.avgMonthlyAcKw)
completion(self.avgMonthlyAcKw)
}
} catch {
print("error: \(error.localizedDescription)")
}
}
dataTask.resume()
}
calling class:
aPVClass.estimateMonthlyACkW(areaSqFt: 100.0, completion: { (monthlyAckW) -> Void in
DispatchQueue.main.async { [weak self] in
guard case self = self else {
return
}
print("monthlyAckW: ", monthlyAckW)
self?.estimatedSolarkWh = Int(monthlyAckW * Double((12)/365 * (self?.numDays)!))
print("estimatedSolarkWh: ", self?.estimatedSolarkWh ?? 0)
guard let val = self?.estimatedSolarkWh else { return }
print("val: ", val)
self?.estimatedSolarkWhLabel.text = String(val)
self?.view.setNeedsDisplay()
}
})
}
monthlyAckW has the right value after completion handler returns. But the assigned value to self?.estimatedSolarkWh is 0, value never gets transferred to the current class scope, UI update fails, even after DispatchQueue.main.async
How to fix this please?
The call of completion is at the wrong place. Move it into the completion closure of the data task after the print line
// prints value
print("updated estimate: ", self.avgMonthlyAcKw)
completion(self.avgMonthlyAcKw)
and delete it after resume
dataTask.resume()
completion(self.avgMonthlyAcKw)
}

Array is null after setting data in it

I have a JSON request that gets data from the Darksky API, I get the data properly and it is showing on the screen. However, When i'm trying to set the data from the array I get from the JSON call in another array, it stays empty.
This is my code:
just declaring the array:
var mForecastArray = [Weather]()
this is the function that calls the API:
func getForecast(){
Weather.forecast(withLocation: "37.8267,-122.4233") { (arr) in
DispatchQueue.main.async {
self.mForecastArray = arr
self.mTodayWeather = arr[0]
self.mCollectionView.reloadData()
}
}
}
The weird part is that it does work, and the data do shows on screen, but still, mForecastArray seems null.
This is the API call itself:
static func forecast(withLocation location: String, completion: #escaping ([Weather]) -> ()){
let url = basePath + location
let request = URLRequest(url: URL(string: url)!)
let task = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in
var forecastArray: [Weather] = []
if let data = data{
do{
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String:Any]{
if let dailyForecast = json["daily"] as? [String:Any]{
if let dailyData = dailyForecast["data"] as? [[String:Any]]{
for dataPoint in dailyData{
if let weatherObject = try? Weather(json: dataPoint){
forecastArray.append(weatherObject)
}
}
}
}
}
}catch{
print(error.localizedDescription)
}
completion(forecastArray)
}
}
task.resume()
}
It's a visual asynchronous illusion.
The static method forecast works asynchronously.
Most likely your code looks like
getForecast()
print(self.mForecastArray)
This cannot work because the array is populated much later.
Move the print line into the completion handler of the static method
func getForecast(){
Weather.forecast(withLocation: "37.8267,-122.4233") { (arr) in
DispatchQueue.main.async {
self.mForecastArray = arr
print(self.mForecastArray)
self.mTodayWeather = arr[0]
self.mCollectionView.reloadData()
}
}
}

Accessing a variable outside a given function

I tried the completion handler as suggested by Gurdev and got something. It does return the values of my array in the MAIN function. But the issue is the same: I have to use Sleep() function for 3 seconds for the HTTP request to complete. This is like hard coding as it could take less or more than 3 seconds to complete. When I remove the Sleep() command, I end up returning a VOID array.
The relevant code is pated below.
Thanks!
--------Web Class--------
import Foundation
import UIKit
class Web {
var ar1 = [Double]()
var ar2 = [Double]()
func getData(str: String, completion: (_ result: [[Double]]) -> Void) {
let request = NSMutableURLRequest(url: URL(string: str)!)
httpGet(request as URLRequest!){
(data, error) -> Void in
if error != nil {
print(error ?? "Error")
} else {
let delimiter = "\t"
let lines:[String] = data.components(separatedBy: CharacterSet.newlines) as [String]
for line in lines {
var values:[String] = []
if line != "" {
values = line.components(separatedBy: delimiter)
let str1 = (values[0])//FIRST COLUMN
let str2 = (values[1])//SECOND COLUMN
let db1 = NumberFormatter().number(from: str1)?.doubleValue
self.ar1.append(db1!)
let db2 = NumberFormatter().number(from: str2)?.doubleValue
self.ar2.append(db2!)
}
}
}
}//end of request
sleep(3) // This delay sends data to MAIN, Otherwise NOT
let dta = [self.ar1, self.ar2]
completion(dta)
}
func httpGet(_ request: URLRequest!, callback: #escaping (String, String?) -> Void) {
let session = URLSession.shared
let task = session.dataTask(with: request, completionHandler: {
(data, response, error) -> Void in
if error != nil {
callback("", error!.localizedDescription)
} else {
let result = NSString(data: data!, encoding:
String.Encoding.ascii.rawValue)!
callback(result as String, nil)
}
})
task.resume()
}
}
--------Web Class--------
-------Call In Class--------
Web().getData (str: "https://dl.dropboxusercontent.com/s/f6j7w7zeqaavzqw/s02.txt?dl=0")
{
(result: [[Double]]) in
let x = result[0]
let y = result[1]
}
-------Call In Class--------
Essentially, I am trying to access my variable "ar1" at a certain point in my code but I cannot. I am trying to return the value too but it returns NULL at that point.
What is wrong here ?
I tried like the below code :
import Foundation
import UIKit
class Web {
var ar1 = [Double]()
var ar2 = [Double]()
func getData(str: String) -> [Double] {
let request = NSMutableURLRequest(url: URL(string: str)!)
httpGet(request as URLRequest!){
(data, error) -> Void in
if error != nil {
print(error ?? "Error")
} else {
let delimiter = "\t"
let lines:[String] = data.components(separatedBy: CharacterSet.newlines) as [String]
for line in lines {
var values:[String] = []
if line != "" {
values = line.components(separatedBy: delimiter)
let str1 = (values[0])//FIRST COLUMN
let str2 = (values[1])//SECOND COLUMN
let db1 = NumberFormatter().number(from: str1)?.doubleValue
self.ar1.append(db1!)
let db2 = NumberFormatter().number(from: str2)?.doubleValue
self.ar2.append(db2!)
}
}
dump (self.ar1) // "ar1" accessible HERE (returns all elements)
}
}//end of request
//BUT I WANT TO RETURN "ar1" HERE !
// So that I can use it in my MAIN class
dump (self.ar1) // "ar1" not accessible here (returns ZERO elements)
return self.ar1
}
func httpGet(_ request: URLRequest!, callback: #escaping (String, String?) -> Void) {
let session = URLSession.shared
let task = session.dataTask(with: request, completionHandler: {
(data, response, error) -> Void in
if error != nil {
callback("", error!.localizedDescription)
} else {
let result = NSString(data: data!, encoding:
String.Encoding.ascii.rawValue)!
callback(result as String, nil)
}
})
task.resume()
}
}
In your code you are returning the ar1 from the return statement of your method whereas the value to ar1 is being set in Aysnchronous callback completion handler. You should use the completion handler block where you are trying to access the value of ar1. HTTPGet function is running in Async mode and value in ar1 is set in the callback handler block.
Let me know if you need any further help on this.

Swift http request use urlSession

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