Can't access data outside of closure in swift - swift

I am trying to extract an array from closure in swift 3 and its not working for me. I have my JSON parser in the class WeatherGetter and I am calling it in the view did load file in the viewcontroller.swift how to assign the weather_data array to some outside variable?
class WeatherGetter {
func getWeather(_ zip: String, startdate: String, enddate: String, completion: #escaping (([[Double]]) -> Void)) {
// This is a pretty simple networking task, so the shared session will do.
let session = URLSession.shared
let string = "Insert API address"
let url = URL(string: string)
var weatherRequestURL = URLRequest(url:url! as URL)
weatherRequestURL.httpMethod = "GET"
// The data task retrieves the data.
let dataTask = session.dataTask(with: weatherRequestURL) {
(data, response, error) -> Void in
if let error = error {
// Case 1: Error
// We got some kind of error while trying to get data from the server.
print("Error:\n\(error)")
}
else {
// Case 2: Success
// We got a response from the server!
do {
var temps = [Double]()
var winds = [Double]()
let weather = try JSON(data: data!)
//print(weather)
let conditions1 = weather["data"]
let conditions2 = conditions1["weather"]
let count = conditions2.count
for i in 0...count-1 {
let conditions3 = conditions2[i]
let conditions4 = conditions3["hourly"]
let count2 = conditions4.count
for j in 0...count2-1 {
let conditions5 = conditions4[j]
let tempF = conditions5["tempF"].doubleValue
let windspeed = conditions5["windspeedKmph"].doubleValue
//temps.updateValue(tempF, forKey: "\(date)//\(j)")
temps.append(tempF)
winds.append(windspeed)
}
}
//print(temps)
//print(winds)
completion([temps, winds])
}
catch let jsonError as NSError {
// An error occurred while trying to convert the data into a Swift dictionary.
print("JSON error description: \(jsonError.description)")
}
}
}
// The data task is set up...launch it!
dataTask.resume()
}
}
override func viewDidLoad() {
super.viewDidLoad()
let weather = WeatherGetter()
weather.getWeather("13323", startdate: "2016-10-01", enddate: "2017-04-30"){(weather_data) -> Void in
print(weather_data[1])
}
//Do your stuff with isResponse variable.
}

You can assign it to a class property like this:
var weatherData: [[Double]]()
override func viewDidLoad() {
super.viewDidLoad()
let weather = WeatherGetter()
weather.getWeather("13323", startdate: "2016-10-01", enddate: "2017-04-30"){(weather_data) -> Void in
self.weatherData = weather_data
// reload or display data
}
}
You need to remember that the network request takes some time, so this is why you would call something like reloadData once you know you have received the response.
Say for example, the network response takes 100 milliseconds to respond. By the time the data has responded, all of the code in viewDidLoad will very likely be completely finished. So you need to respond to the data being received, when you receive it. If you have a bad mobile signal, it may take longer.
This is why you use callbacks/closures. They are called when the operation completes
UPDATE:
The code inside getWeather shows multiple errors for me and won't let me run it as is.
I managed to get a response from the weather API by modifying the code slightly and commenting alot out. Your main issue here is that you are not casting your JSON data to specific types.
// The data task retrieves the data.
let dataTask = session.dataTask(with: weatherRequestURL) {
(data, response, error) -> Void in
guard error == nil, let data = data else {
print("ERROR")
return
}
// Case 2: Success
// We got a response from the server!
do {
var temps = [Double]()
var winds = [Double]()
if let weather = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String:AnyObject] {
if let conditions1 = weather["data"] as? [String:AnyObject] {
print(conditions1)
}
}
} catch let jsonError {
// An error occurred while trying to convert the data into a Swift dictionary.
print("JSON error description: \(jsonError)")
}
}
dataTask.resume()
See in the code above how I am optionally unwrapping the values whilst casting their types. This is what you need to do throughout your code and check you get the right data at each step along the way. Unfortunately the API response is too large for me to do it here.
Unwrapping JSON Swift
Swift Closures
iOS Networking with Swift - This is a free course which I highly recommend. This is how I learnt iOS networking.

As mentioned by #Scriptable, it takes a while for the response to be processed since it's asynchronous. What you can do is to add the OperationQueue.main.addOperation to assign the current process to the main queue. This will prioritize the processing of your network response. You can also put your reloadData in this part.
var weatherData: [Double]()
override func viewDidLoad() {
super.viewDidLoad()
let weather = WeatherGetter()
OperationQueue.main.addOperation {
weather.getWeather("13323", startdate: "2016-10-01", enddate: "2017-04-30"){(weather_data) -> Void in
print(weather_data[1])
// reloadData()
}
}
}

Related

How to wait until data from network call comes and only then return value of a function #Swift

I have a service class that makes an api call and stores data into its property. Then my interactor class have a method where I want to make service class api call and when data will be stored - return it. I tried myself to handle this with completion handler and dispatch group, but (I suppose I just missing something) this didn't work. I would be very appreciated if you help me to deal with this problem. Thanks in advance!
Service class:
class PunkApiService{
var beers = [Beer]()
func loadList(at page: Int){
//MARK: - Checks is URL is valid + pagination
guard let url = URL(string: "https://api.punkapi.com/v2/beers?page=\(page)&per_page=25") else {
print("Invalid URL")
return
}
//MARK: - Creating URLSession DataTask
let task = URLSession.shared.dataTask(with: url){ data, response, error in
//MARK: - Handling no erros came
guard error == nil else {
print(error!)
return
}
//MARK: - Handling data came
guard let data = data else{
print("Failed to load data")
return
}
do{
let beers = try JSONDecoder().decode([Beer].self, from: data)
self.beers.append(contentsOf: beers)
}
catch{
print("Failed to decode data")
}
}
task.resume()
}
And Interactor class(without completion handler or dispatch group):
class BeersListInteractor:BeersListInteractorProtocol{
private var favoriteBeers = FavoriteBeers()
private var service = PunkApiService()
//MARK: - Load list of Beers
func loadList(at page: Int) -> [Beer]{
service.loadList(at: page)
return service.beers
}
Added: my attempt with completion handler
var beers: [Beer]
func loadList(at page: Int, completion: ()->()){
service.loadList(at: page)
completion()
}
func completion(){
beers.append(contentsOf: service.beers)
}
loadList(at: 1) {
completion()
}
This is what async/await pattern is for, described here. In your case both loadList functions are async, and the second one awaits for the first one:
class PunkApiService {
func loadList(at page: Int) async {
// change function to await for task result
let (data, error) = try await URLSession.shared.data(from: url)
let beers = try JSONDecoder().decode([Beer].self, from: data)
...
return beers
}
}
class BeersListInteractor: BeersListInteractorProtocol {
func loadList(at page: Int) async -> [Beer]{
let beers = await service.loadList(at: page)
return service.beers
}
}
See a good explanation here
I think that you were on the right path when attempting to use a completion block, just didn't do it correctly.
func loadList(at page: Int, completion: #escaping ((Error?, Bool, [Beer]?) -> Void)) {
//MARK: - Checks is URL is valid + pagination
guard let url = URL(string: "https://api.punkapi.com/v2/beers?page=\(page)&per_page=25") else {
print("Invalid URL")
completion(nil, false, nil)
return
}
//MARK: - Creating URLSession DataTask
let task = URLSession.shared.dataTask(with: url){ data, response, error in
//MARK: - Handling no erros came
if let error = error {
completion(error, false, nil)
print(error!)
return
}
//MARK: - Handling data came
guard let data = data, let beers = try? JSONDecoder().decode([Beer].self, from: data) else {
completion(nil, false, nil)
return
}
completion(nil, true, beers)
}
task.resume()
}
This is the loadList function, which now has a completion parameter that will have three parameters, respectively the optional Error, the Bool value representing success or failure of obtaining the data, and the actual [Beers] array, containing the data (if any was retrieved).
Here's how you would now call the function:
service.loadList(at: page) { error, success, beers in
if let error = error {
// Handle the error here
return
}
if success, let beers = beers {
// Data was correctly retrieved - and safely unwrapped for good measure, do what you need with it
// Example:
loader.stopLoading()
self.datasource = beers
self.tableView.reloadData()
}
}
Bear in mind the fact that the completion is being executed asynchronously, without stopping the execution of the rest of your app.
Also, you should decide wether you want to handle the error directly inside the loadList function or inside the closure, and possibly remove the Error parameter if you handle it inside the function.
The same goes for the other parameters: you can decide to only have a closure that only has a [Beer] parameter and only call the closure if the data is correctly retrieved and converted.

Why my DateTask code block does not work?

I create a request to the server, and in the end I expect to receive data, which I then transform into a model using a function, for this I created a session
func fetchNewsData(forCoutry country: String, category: String, complition: #escaping (NewsDataModel) -> ()) {
let urlString = "some url string"
guard let url = URL(string: urlString) else { return }
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { data, response, error in
print ("ERROR: \(error)")
guard let data = data else { return }
guard let newsData = self.parseJSON(withData: data) else { return }
complition(newsData)
}
task.resume()
}
but the following code just doesn't work
print ("ERROR: \(error)")
guard let data = data else { return }
guard let newsData = self.parseJSON(withData: data) else { return }
complition(newsData)
I used breakpoints to find out until what point everything is going well, and I realized that this particular block of code is not working.
when I set a breakpoint between the let session and the let task, the code stopped there, but when I set my code to an print(error), this breakpoint did not work
I used the function fetchNewsData in viewDidLoad and I want to work to fill the array with elements that I expect to receive from the data that will come on this request, but my array does not receive any elements, and it remains empty, because of this my application does not work
why part of the code doesn't work, and how can I get the data I need from it?
The problem turned out to be a poor understanding of closures
I was not calling my method correctly to get the data. Having figured it out, I realized that the problem is precisely in a different approach when calling this method

Getting data out of a completion handler and into a tableView

I've been trying to understand this process, I've done a lot of reading and it's just not clicking so I would be grateful if anyone can break this down for me.
I have a method to retrieve JSON from a URL, parse it, and return the data via a completion handler. I could post code but it's all working and I (mostly) understand it.
In my completion handler I can print the data in the console so I know it's there and everything good so far.
The next bit is what's tripping me up. While I can use the data in the completion handler I can't access it from the view controller that contains the handler.
I want to be able to pass tableData.count to numberOfRows and get "Use of unresolved identifier 'tableData'"
I'd really appreciate it if anyone can lay out what I need to do next. Thanks!
Edit: adding code as requested
Here is my completion handler, defined in the ViewController class:
var tableData: [Patient] = []
var completionHandler: ([Patient]) -> Void = { (patients) in
print("Here are the \(patients)")
}
in viewDidLoad:
let url = URL(string: "http://***.***.***.***/backend/returnA")
let returnA = URLRequest(url: url!)
retrieveJSON(with: returnA, completionHandler: completionHandler)
Defined in Networking.swift file:
func retrieveJSON(with request: URLRequest, completionHandler: #escaping ([Patient]) -> Void) {
// set up the session
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
// make the request
let task = session.dataTask(with: request as URLRequest) {
// completion handler argument
(data, response, error) in
// completion handler
guard let data = data else {
print("Did not recieve data")
completionHandler([])
return
}
do {
let decoder = JSONDecoder()
let Patient = try decoder.decode(Array<Patient>.self, from: data)
// print(Patient)
completionHandler(Patient)
}
catch let err {
print("Err", err)
completionHandler([])
}
}
task.resume()
}
I also have a struct defined called Patient but I won't post that as it's very long and just a simple struct matching the JSON received.
First of all, when you use closure, you should consider strong reference cycle.
let completionHandler: ([Patient]) -> Void = { [weak self] patients in
guard let strongSelf = self else { return }
strongSelf.tableData = patients // update tableData that must be used with UITableViewDataSource functions.
strongSelf.tableView.reloadData() // notify tableView for updated data.
}
You are not populating the array(tableData) in the closure:
var completionHandler: ([Patient]) -> Void = {[weak self] (patients) in
print("Here are the \(patients)")
self?.tableData = patients
}
var tableData: [Patient] = []
var completionHandler: ([Patient]) -> Void = { (patients) in
self.tableData = patients
self.tableView.reloadData()
//make sure your tableview datasource has tableData property used
}

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

Swift3 Cannot assign value of type '()' to type '[Version]'

Hello I am having trouble calling my methods to the controller properly as I am getting this error Cannot assign value of type '()' to type '[Version]'. I need help fixing this, thanks.
Swift 3 Method:
var versions : [Version] = []
func isActiveVersion() -> Bool {
let api = versionAPI()
versions = api.getVersionFromAPI(completion: ({_ in }))
for version in versions{
if version["version"] == "1.0.0" {
return true
}
}
}
Swift 3 Call
public class versionAPI {
var versions : [Version] = []
//---------------------------------
// MARK: ENDPOINTS
//---------------------------------
let getVersionEndPoint = "http://127.0.0.1:3000/api/v1/versions"
//---------------------------------
// MARK: REQUESTS
//---------------------------------
func getVersionFromAPI(completion: #escaping ([Version]) -> Void){
let url = URL(string: getVersionEndPoint)
let task = URLSession.shared.dataTask(with: url! as URL) { data, response, error in
guard let data = data, error == nil else {
completion([])
return
}
print(NSString(data: data, encoding: String.Encoding.utf8.rawValue)!)
self.parseVersionsToJSON(data: data)
completion(self.versions)
}
task.resume()
}
func parseVersionsToJSON(data: Data) {
do {
self.versions = []
if let json = try JSONSerialization.jsonObject(with: data) as? [[String:Any]] {
for dic in json {
let version = Version()
version.version = dic["version"] as! String
version.active = dic["active"] as! Bool
self.versions.append(version)
}
}
}
catch{
}
}
}
Your function getVersionFromAPI sets up an asynchronous task and then immediately returns before that task completes, returning void to its caller. This is why you get the error you report.
The [Version] value produced by the asynchronous task is passed by that task to the completion function passed to getVersionFromAPI. The completion function you pass {_ in } does nothing, so the list of versions is simply discarded.
You cannot simply call an asynchronous task, which will complete at some future time, from a synchronous task, getVersionFromAPI in your case, and have that asynchronous task somehow become synchronous and return its future result immediately.
You need to study asynchronous design. Then either redesign your code so the task done by getVersionFromAPI is itself asynchronous, or use one of the standard techniques to block your synchronous method from proceeding until the asynchronous one has completed.
If after revising your design you have trouble with your code ask a new question, showing your code, and someone will undoubtedly help you.
HTH
versions = api.getVersionFromAPI(completion: ({_ in }))
getVersionFromAPI does not return anything. Declare a global struct then pass the data into it and use DispatchQueue when finished to post a NotificationCentre
do {
GlobalStruct.versions = []
if let json = try JSONSerialization.jsonObject(with: data) as? [[String:Any]] {
for dic in json {
let version = Version()
version.version = dic["version"] as! String
version.active = dic["active"] as! Bool
GlobalStruct.versions.append(version)
}
DispatchQueue.main.async {
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "gotIt"), object: nil)
}
}
}
your Swift 3 ViewController should have the following:
var versions: [Version] = []
func viewDidLoad() {
NotificationCenter.default.addObserver(self, selector: #selector(myFunction), name: NSNotification.Name(rawValue: "gotIt"), object: nil)
let api = versionAPI()
api.getVersionFromAPI()
super.viewDidLoad()
}
func myFunction() {
versions = GlobalStruct.versions
if isActiveVersion {
.....
}
}