Array is null after setting data in it - swift

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()
}
}
}

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.

completion handler always returns an empty array except when called from code that populate array

Im having some difficulties with completition handler. I don't want to use new async/await feature untill learning the old way.
This is my #escaping fetch() func:
func fetch(completion: #escaping (Result<[CountriesFinal], CountryError>) -> Void) {
let urlString = "https://raw.githubusercontent.com/fdsfdghjfvghdfhf.json"
let url = URL(string: urlString)!
let _ = URLSession.shared.dataTask(with: url) { data, _, _ in
guard let jsonData = data else {
completion(.failure(.noDataAvailable))
return
}
do {
let decoder = JSONDecoder()
let countriesResponse = try decoder.decode(Country.self, from: jsonData)
let countryDetails = countriesResponse.countries
completion(.success(countryDetails))
} catch {
completion(.failure(.canNotProcessData))
}
}.resume()
}
I've learned that fetch() should be called like this inside viewDidLoad():
// var countriess = [CountriesFinal]() // outside of viewDidLoad
fetch { result in
switch result {
case let .failure(error):
print(error)
case let .success(holidays):
self.countriess = holidays
print(self.countriess[0].name) // WORKS ✔️
}
}
// If written here or in any other func:
print(self.countriess[0].name) // Thread 1: Fatal error: Index out of range

Swift calling completion handler in from another file fails

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 :)

Swift: Setting the text of a label in a URLSessionTask

So I am downloading a JSON file using a URLRequest().
I parse through it in order to get a specific string and I want to set the text of a label I have in my ViewController to that specific string.
I use a CompletionHandler in order to retrieve the function that gets the JSON file from another Swift file.
Here is the code of calling the function and setting the label:
class SecondViewController: UIViewController {
tr = TransportServices()
tr.getLyftData(origin: originstring, destination: destinationstring){ json in
//Parsing JSON in order to get specific data
self.lyftlabel.text = stringexample
}
}
and here is the code of getting the JSON
func getLyftData(origin: String, destination: String, completionHandler: #escaping ([String: Any]) -> ()){
let urlrequest = URLRequest(url: URL(string: urlstring)!)
let config = URLSessionConfiguration.default
let sessions = URLSession(configuration: config)
let task = sessions.dataTask(with: urlrequest) {(data, response, error) in
guard error == nil else {
print(error!)
return
}
guard let responseData = data else {
print("error, did not receive data")
return
}
do {
if let json = try JSONSerialization.jsonObject(with: responseData, options: []) as? [String: Any]{
completionHandler(json)
}
}
catch {
print("Error with URL Request")
}
}
task.resume()
}
This does the job, but in a very slow manner. I know that there is a runtime issue because UILabel.text must be set from main thread only, but I don't know any other way to fix it. Please help.
If you want to set label text in main thread use this:
DispatchQueue.main.async {
self.lyftlabel.text = stringexample
}

In method A, get data from callback in method B?

I have a function that is build to get the latest items from a API. There are several other ones, with different functionality, but they all work the same. It looks like this:
func getLatest(pageNumber: Int) -> Array<Any>{
let urlRequest = URL(string: baseUrl + latestUrl + String(pageNumber))
let requestedData = doRequest(url: urlRequest!, completion: { data -> Void in
// We have the data from doRequest stored in data, but now what?!
})
return allData
}
I also have a async method that handles the requests. That one looks like this:
func doRequest(url: URL, completion: #escaping ([[ApiItem]]) -> ()){
var allItems = [[ApiItem]]()
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
do{
let json = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as! [String: AnyObject]
let results = json["items"] as? [AnyObject]
for r in results!{
let item = ApiItem(json: r as! [String: Any])
allItems.append([item])
}
completion(allItems)
} catch let jsonError{
print("JSON error: \(jsonError)")
}
}.resume()
The doRequest function works absolutely fine. It gets the data, parses the JSON and send it back to getLatest --> requestedData. The problem right now is, is that getLatest() is a function that needs to return the data that is stored in the data variable of requestedData.
How can I make it so, that the getLatest() function returns the data that is stored in the data in requestedData()?
So I've fixed it by doing this:
In the first method, the one that actually needs the data from the API, I added this:
let trendingData = restApiManager.getLatest(pageNumber: 0, completion: { data -> Void in
let item = data[indexPath.row]
let url = NSURL(string: item.still)
let data = NSData(contentsOf: url as! URL)
if data != nil {
cell.image.image = UIImage(data:data! as Data)
}
})
The getLatest() method looks like this:
func getLatest(pageNumber: Int, completion: #escaping ([ApiItem]) -> ()) {
let urlRequest = URL(string: baseUrl + trendingUrl + String(pageNumber))
let requestedData = doRequest(url: urlRequest!, completion: { data -> Void in
// We have the data from doRequest stored in data
var requestedData = data
completion(requestedData)
})
}
And finally, the doRequest() method looks like this:
func doRequest(url: URL, completion: #escaping ([ApiItem]) -> ()){
var allItems = [ApiItem]()
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
do{
let json = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as! [String: AnyObject]
let results = json["items"] as? [AnyObject]
for r in results!{
let item = ApiItem(json: r as! [String: Any])
allItems.append(item)
}
completion(allItems)
} catch let jsonError{
print("JSON error: \(jsonError)")
}
}.resume()
}
What I would do is use a Singleton in which I can store the Data
class DataManager:NSObject
{
static let instance = DataManager()
override private init(){}
var items:[ApiItem] = []
}
Then in your first method I would do this:
func getLatest(pageNumber: Int){
let urlRequest = URL(string: baseUrl + latestUrl + String(pageNumber))
let requestedData = doRequest(url: urlRequest!, completion: { data -> items in
// We have the data from doRequest stored in data, but now what?!
DataManager.instance.items = items
})
}
This is how I usually go about this kind of situations. There may be better options though...