Modify Global Variable Inside Closure (Swift 4) - ios

I am trying to modify the global variable currentWeather (of type CurrentWeather) using this function, which is meant to update said variable with the information retrieved from the URL and return a bool signifying its success. However, the function is returning false, as currentWeather is still nil. I recognize that the dataTask is asynchronous, and that the task is running in the background parallel to the application, but I don't understand what this means for what I'm trying to accomplish. I also am unable to update currentWeather after the do block, as weather is no longer recognized after exiting the block. I did try using "self.currentWeather", but was told it was an unresolved identifier (perhaps because the function is also global, and there is no "self"?).
The URL is not currently valid because I took out my API key, but it is working as expected otherwise, and my CurrentWeather struct is Decodable. Printing currentWeatherUnwrapped is also consistently successful.
I did look around Stack Overflow and through Apple's official documentation and was unable to find something that answered my question, but perhaps I wasn't thorough enough. I'm sorry if this is a duplicate question. Direction to any further relevant reading is also appreciated! I apologize for the lack of conformity to best coding practices - I'm not very experienced at this point. Thank you all so much!
func getCurrentWeather () -> Bool {
let jsonUrlString = "https://api.wunderground.com/api/KEY/conditions/q/\(state)/\(city).json"
guard let url = URL(string: jsonUrlString) else { return false }
URLSession.shared.dataTask(with: url) { (data, response, err) in
// check error/response
guard let data = data else { return }
do {
let weather = try JSONDecoder().decode(CurrentWeather.self, from: data)
currentWeather = weather
if let currentWeatherUnwrapped = currentWeather {
print(currentWeatherUnwrapped)
}
} catch let jsonErr {
print("Error serializing JSON: ", jsonErr)
}
// cannot update currentWeather here, as weather is local to do block
}.resume()
return currentWeather != nil
}

When you do an asynchronous call like this, your function will return long before your dataTask will have any value to return. What you need to do is use a completion handler in your function. You can pass it in as a parameter like this:
func getCurrentWeather(completion: #escaping(CurrentWeather?, Error?) -> Void) {
//Data task and such here
let jsonUrlString = "https://api.wunderground.com/api/KEY/conditions/q/\(state)/\(city).json"
guard let url = URL(string: jsonUrlString) else { return false }
URLSession.shared.dataTask(with: url) { (data, response, err) in
// check error/response
guard let data = data else {
completion(nil, err)
return
}
//You don't need a do try catch if you use try?
let weather = try? JSONDecoder().decode(CurrentWeather.self, from: data)
completion(weather, err)
}.resume()
}
Then calling that function looks like this:
getCurrentWeather(completion: { (weather, error) in
guard error == nil, let weather = weather else {
if weather == nil { print("No Weather") }
if error != nil { print(error!.localizedDescription) }
return
}
//Do something with your weather result
print(weather)
})

All you need is a closure.
You cant have synchronous return statement to return the response of web service call which in itself is asynchronous in nature. You need closures for that.
You can modify your answer as below. Because you have not answered to my question in comment I have taken liberty to return the wether object rather than returning bool which does not make much sense.
func getCurrentWeather (completion : #escaping((CurrentWeather?) -> ()) ){
let jsonUrlString = "https://api.wunderground.com/api/KEY/conditions/q/"
guard let url = URL(string: jsonUrlString) else { return false }
URLSession.shared.dataTask(with: url) { (data, response, err) in
// check error/response
guard let data = data else { return }
do {
let weather = try JSONDecoder().decode(CurrentWeather.self, from: data)
CurrentWeather.currentWeather = weather
if let currentWeatherUnwrapped = currentWeather {
completion(CurrentWeather.currentWeather)
}
} catch let jsonErr {
print("Error serializing JSON: ", jsonErr)
completion(nil)
}
// cannot update currentWeather here, as weather is local to do block
}.resume()
}
Assuming currentWeather is a static variable in your CurrentWeather class you can update your global variable as well as return the actual data to caller as shown above
EDIT:
As pointed out by Duncan in comments below, the above code executes the completion block in background thread. All the UI operations must be done only on main thread. Hence its very much essential to switch the thread before updating the UI.
Two ways :
1- Make sure you execute the completion block on main thread.
DispatchQueue.main.async {
completion(CurrentWeather.currentWeather)
}
This will make sure that whoever uses your getCurrentWeather in future need not worry about switching thread because your method takes care of it. Useful if your completion block contains only the code to update UI. Lengthier logic in completion block with this approach will burden the main thread.
2 - Else In completion block that you pass as a parameter to getCurrentWeather whenever you update UI elements make sure you wrap those statements in
DispatchQueue.main.async {
//your code to update UI
}
EDIT 2:
As pointed out by Leo Dabus in comments below, I should have run completion block rather than guard let url = URL(string: jsonUrlString) else { return false } That was a copy paste error. I copied the OP's question and in a hurry din realize that there is a return statement.
Though having a error as a parameter is optional in this case and completely depends on how you designed your error handling model, I appreciate the idea suggested by Leo Dabus which is more general approach and hence updating my answer to have error as a parameter.
Now there are cases where we may need to send our custom error as well for example if guard let data = data else { return } returns false rather than simply calling return you may need to return a error of your own which says invalid input or something like that.
Hence I have taken a liberty to declare a custom errors of my own and you can as well use the model to deal with your error handling
enum CustomError : Error {
case invalidServerResponse
case invalidURL
}
func getCurrentWeather (completion : #escaping((CurrentWeather?,Error?) -> ()) ){
let jsonUrlString = "https://api.wunderground.com/api/KEY/conditions/q/"
guard let url = URL(string: jsonUrlString) else {
DispatchQueue.main.async {
completion(nil,CustomError.invalidURL)
}
return
}
URLSession.shared.dataTask(with: url) { (data, response, err) in
// check error/response
if err != nil {
DispatchQueue.main.async {
completion(nil,err)
}
return
}
guard let data = data else {
DispatchQueue.main.async {
completion(nil,CustomError.invalidServerResponse)
}
return
}
do {
let weather = try JSONDecoder().decode(CurrentWeather.self, from: data)
CurrentWeather.currentWeather = weather
if let currentWeatherUnwrapped = currentWeather {
DispatchQueue.main.async {
completion(CurrentWeather.currentWeather,nil)
}
}
} catch let jsonErr {
print("Error serializing JSON: ", jsonErr)
DispatchQueue.main.async {
completion(nil,jsonErr)
}
}
// cannot update currentWeather here, as weather is local to do block
}.resume()
}

You fundamentally misunderstand how async functions work. You function returns before the URLSession's dataTask has even begun to execute. A network request may take multiple seconds to complete. You ask it to fetch some data for you, give it a block of code to execute ONCE THE DATA HAS DOWNLOADED, and then go on with your business.
You can be certain that the line after the dataTask's resume() call will run before the new data has loaded.
You need to put code that you want to run when the data is available inside the data task's completion block. (Your statement print(currentWeatherUnwrapped) will run once the data has been read successfully.)

As you pointed out, the data ask is async, meaning you do not know when it will be completed.
One option is to modify your wrapper function getCurrentWeather to be async as well by not providing a return value, but instead a callback/closure. Then you will have to deal with the async nature somewhere else though.
The other option which is what you probably want in your scenario is to make the data task synchronous like so:
func getCurrentWeather () -> Bool {
let jsonUrlString = "https://api.wunderground.com/api/KEY/conditions/q/\(state)/\(city).json"
guard let url = URL(string: jsonUrlString) else { return false }
let dispatchGroup = DispatchGroup() // <===
dispatchGroup.enter() // <===
URLSession.shared.dataTask(with: url) { (data, response, err) in
// check error/response
guard let data = data else {
dispatchGroup.leave() // <===
return
}
do {
let weather = try JSONDecoder().decode(CurrentWeather.self, from: data)
currentWeather = weather
if let currentWeatherUnwrapped = currentWeather {
print(currentWeatherUnwrapped)
}
dispatchGroup.leave() // <===
} catch let jsonErr {
print("Error serializing JSON: ", jsonErr)
dispatchGroup.leave() // <===
}
// cannot update currentWeather here, as weather is local to do block
}.resume()
dispatchGroup.wait() // <===
return currentWeather != nil
}
The wait function can take parameters, which can define a timeout. https://developer.apple.com/documentation/dispatch/dispatchgroup Otherwise your app could be stuck waiting forever. You will then be able to define some action to present that to the user.
Btw I made a fully functional weather app just for learning, so check it out here on GitHub https://github.com/erikmartens/NearbyWeather. Hope the code there can help you for your project. It's also available on the app store.
EDIT: Please understand that this answer is meant to show how to make async calls synchronous. I am not saying this is good practice for handling network calls. This is a hacky solution for when you absolutely must have a return value from a function even though it uses async calls inside.

Related

UI freezes when using FirestoreDecoder to decode Firestore documents

I'm fetching data from Firestore, mapping the documents and decoding each of them using FirestoreDecoder. However, decoding the documents momentarily freezes the UI. Running the code on the background thread makes no difference. How can I prevent the UI from freezing during the decoding?
let collection = Firestore.firestore().collection("roll_groups")
collection.addSnapshotListener { (snapshot, error) in
if let error = error {
print("Error fetching roll groups: \(error.localizedDescription)")
} else if let snapshot = snapshot {
DispatchQueue.global(qos: .background).async {
let rollGroups = snapshot.documents.map { doc -> RollGroup? in
do {
let rollGroup = try FirestoreDecoder().decode(RollGroup.self, from: doc.data())
return rollGroup
} catch {
print("Error decoding roll groups: \(error)")
return nil
}
}
DispatchQueue.main.async {
completion(rollGroups)
}
}
}
}
Possible Solution
Looking at this code it all seems fine, I just want to confirm that the completion for your method is definitely a #escaping: completion() otherwise it could cause this issue
also, it might be worth wrapping the actual DB call (collection.addSnapshotListener) in
DispatchQueue.global(qos: .background).async
Just to see if that Makes a difference, in theory it shouldn't but nonetheless it's worth a shot

Images not going straight into array

I have a small problem with some code here. I am trying to populate a collection view with Five Names, descriptions and Images.
I am able to successfully to download all of the above into their respected arrays.
The problem is that the first time I perform the segue the image array has zero values in it. Then I go back a page and re-enter the page to find that all of the arrays have been populated successfully....
This is really annoying. Here is my code:
//arrays of names, descriptions and images
var names:[String] = []
var descriptions: [String] = []
var imagesArray: [UIImage] = []
Heres where I get the images:
func downloadImages(){
for x in 1...5{
let url = URL(string: "https://www.imagesLocation.com/(x).png")
let task = URLSession.shared.dataTask(with: url!){(data, response, error) in
guard
let data = data,
let newImage = UIImage(data: data)
else{
print("Could not load image from URL: ",url!)
return
}
DispatchQueue.main.async {
self.imagesArray.append(newImage)
}
}
task.resume()
}
loadDataFromFirebase()
}
Heres where I download the Names and Descriptions from:
func loadDataFromFirebase() {
// Fetch and convert data
let db = Firestore.firestore()
db.collection(self.shopName).getDocuments { (snapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
return
} else {
for document in snapshot!.documents {
let name = document.get("Name") as! String
let description = document.get("Description") as! String
self.names.append(name)
self.descriptions.append(description)
}
self.setupImages() //safe to do this here as the firebase data is valid
}
}
}
Heres where I setup the collection view with the Names, Description and Images array contents:
func setupImages(){
do {
if imagesArray.count < 5 || names.count < 5 || descriptions.count < 5 {
throw MyError.FoundNil("Something hasnt loaded")
}
self.pages = [
Page(imageName: imagesArray[0], headerText: names[0], bodyText: descriptions[0]),
Page(imageName: imagesArray[1], headerText: names[1], bodyText: descriptions[1]),
Page(imageName: imagesArray[2], headerText: names[2], bodyText: descriptions[2]),
Page(imageName: imagesArray[3], headerText: names[3], bodyText: descriptions[3]),
Page(imageName: imagesArray[4], headerText: names[4], bodyText: descriptions[4]),
]
}
catch {
print("Unexpected error: \(error).")
}
}
As you can see from the image below, every array is populating successfully apart from the images array:
Here is the segue from the previous page's code:
DispatchQueue.main.async(){
self.performSegue(withIdentifier: "goToNext", sender: self)
}
Any help is welcome :)
Your question is just a variant of the classic, "Why is my asynchronous function returning empty data?" I've answered a couple of these questions, and I'll include an analogy that explains the issue. You might understand the issue already, but I'll include it anyway for future readers:
Your mom is cooking dinner and asks you to go buy a lemon.
She starts cooking, but she has no lemon!
Why? Because you haven't yet returned from the supermarket, and your
mom didn't wait.
Source
The main issue here is that you are calling loadDataFromFirebase way too early. You assume that it will execute only after your URL requests have completed, but that is not the case. Why? Because the URL requests are executed asynchronously. That is, they run on another thread instead of blocking the thread that calls dataTask.resume. This is why, as Shashank Mishra suggests, you should use a DispatchGroup. Additionally, there is no guarantee that your images will load in the order that you begin the data tasks. I have included a fix below.
Generally, I would recommend defining variables strictly in the scopes in which you need them. Keeping names, descriptions, and images at such a high scope makes it too easy to make mistakes. I suggest refactoring your functions and deleting those three class-level arrays. Instead:
func loadDataFromFirebase(images: [UIImage]) {
// same function as you posted, except make names and descriptions local variables and
// replace self.setupImages() with:
DispatchQueue.main.async {
self.setupImages(images: images, names: names, descriptions: descriptions)
}
}
func setupImages(images: [UIImage], names: [String], descriptions: [String]) {
guard images.count == 5, names.count == 5, descriptions.count == 5 else {
print("Missing data.")
return
}
self.pages = (0..<5).map({ Page(image: images[$0], header: names[$0], body: descriptions[$0]) })
// super important!!!
tableView.reloadData()
}
Finally, here is my suggestion for a thread-safe downloadImages function:
func downloadImages() {
var images = [UIImage?](repeating: nil, count: 5)
let dispatchGroup = DispatchGroup()
for i in 1...5 {
dispatchGroup.enter()
let url = URL(string: "https://www.imagesLocation.com/\(i).png")!
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data, let image = UIImage(data: data) else {
print("Could not load image from", url)
dispatchGroup.leave()
return
}
images[i] = image
dispatchGroup.leave()
}.resume()
}
dispatchGroup.notify(queue: .main) {
guard images.allSatisfy({$0 != nil}) else {
print("Failed to fetch all images.")
return
}
self.loadDataFromFirebase(images: images.compactMap({$0}))
}
}
As Fattie pointed out, you should use addSnapshotListener rather than getDocuments. Also, you should add the listener/get documents while downloading the images instead of after, which will be faster. However, I am not adding either to my answer because this is already quite long, and if you have trouble with it you can post another question.
You can use DispatchGroup to achieve asynchronous calls -
func downloadImages() {
let dispatchGroup = DispatchGroup()
for x in 1...5 {
dispatchGroup.enter()
let url = URL(string: "https://www.imagesLocation.com/(x).png")
let task = URLSession.shared.dataTask(with: url!){(data, response, error) in
guard
let data = data,
let newImage = UIImage(data: data)
else{
print("Could not load image from URL: ",url!)
dispatchGroup.leave()
return
}
self.imagesArray.append(newImage)
dispatchGroup.leave()
}
task.resume()
}
dispatchGroup.notify(queue: DispatchQueue.main) {
self.loadDataFromFirebase()
}
}
Call "loadDataFromFirebase()" method on getting all 5 responses as above. It will always have all images before loading it on view.
You're misunderstanding how Firebase works.
Essentially.
Don't use getDocuments. Use .addSnapshotListener
and
Basically each time the snapshot arrives, simply call .reloadData() on the table.
A full tutorial is beyond the scope of an answer here but there are many, many, tutorials around.
Just a typical fragment ...
let db = Firestore.firestore().db.collection("yourCollection")
.whereField("user", isEqualTo: uid)
.addSnapshotListener { [weak self] documentSnapshot, error in
guard let self = self else { return }
guard let ds = documentSnapshot else {
return print("error: \(error!)")
}
self.displayItems = .. that data
self.tableView.reloadData()
}
Note the .reloadData()
Also ..
It's true that you can store an image (binary data) right in Firestore.
But really never, ever, do that - it's completely useless.
Simply use the dead-easy Firebase/Storage system where you can host images for free. Then they have completely normal URLs and so on.
Full tutorial: https://stackoverflow.com/a/62626214/294884

Swift 5 Coordinating background tasks

I am still trying to understand and control asynch tasks. I have an app which is generating multiple API calls to different providers. I could have a maximum of 367 API calls in parallel. How can I coordinate all of these so I can know when the first one starts and the last one is completed?
With help from this forum, I can get this working with single calls, but not multiple.
My call to the API from a class is below (relevant sections, which show the use of .completion:
let session = URLSession.shared
let url = components.url!
let request = URLRequest(url: url)
session.dataTask(with: request) { data, response, error in
if let error = error {
DispatchQueue.main.async {
completion(.failure(error))
}
return
}
guard
let responseData = data,
let httpResponse = response as? HTTPURLResponse,
200 ..< 300 ~= httpResponse.statusCode
else {
DispatchQueue.main.async {
completion(.failure(AstronomicalTimesError.invalidResponse(data, response)))
}
return
}
do {
print("Astronomical times api completed with status code ", httpResponse.statusCode)
let astronomicalTimesResponse = try JSONDecoder().decode(AstronomicalTimesResponse.self, from: responseData)
DispatchQueue.main.async {
completion(.success(astronomicalTimesResponse))
//print("astronomical times loaded ", astronomicalTimesResponse)
}
} catch let jsonError {
DispatchQueue.main.async {
completion(.failure(jsonError))
}
}
}.resume()
This is then called for each day in a date range.
repeat {
let astronomicalTimes = AstronomicalTimes(date: astRetrievalDate, latitude: station.lat, longitude: station.long)
astronomicalTimes.start { result in
switch result {
case .success(let astronomicalTimesResponse):
let detail = DayDetails(date: astRetrievalDate, astronomicalTimes: astronomicalTimesResponse.results)
details.append(detail)
case .failure(let error):
print("Astronomical Times API call from saved tides failed with error \(error)")
}
}
astRetrievalDate = astRetrievalDate.dayAfter
} while astRetrievalDate.noon <= toDate.noon
let tideToSave = SavedTides(saveKey: key, details: details)
savedTides.append(tideToSave)
print("saved tide details: ", savedTides)
I want to build up the results in savedTides but, the all the API's complete after the assignment to savedTides so this is always empty. Note that I will also be firing off another two separate API's to different providers so I need all of these to complete before I assign the results of all of them to an array.
Why don't you have a look at promises? so you can fire other call whenever the others that you need to complete before are done.
https://github.com/mxcl/PromiseKit
You can group multiple promises in an array and fire them all together, or maybe a DispatchGroup could help you.
Thanks all. DispatchGroup was the answer. I was really struggling to understand how to use it from the apple documentation, however I came across this excellent tutorial and all was clear. How to use DispatchGroup in Swift 4.2

'[ResponseData]' to expected argument type 'ResponseData'

I'm creating an application which are fetching data from an API. I've created the "API call" in a separate class so I can use the same call multiple times. But it does not return the value as I expect it to.
In ViewController A
let data = JsonData.init()
data.downloadJsonData(urlString: urlString) { (responseArray) in
dataArray.append(responseArray)
print(self.dataArray)
}
I'm getting the error at dataArray.append(responseArray):
Cannot convert value of type '[ResponseData]' to expected argument type 'ResponseData'
In JsonData class
class JsonData{
var dataArray:[ResponseData] = []
func downloadJsonData(urlString: String, completed: #escaping (Array<ResponseData>) -> ()){
guard let url = URL(string: urlString) else {return}
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else{
return
}
do{
self.dataArray = [try JSONDecoder().decode(ResponseData.self, from: data)]
//Complete task in background
DispatchQueue.main.async {
completed(self.dataArray)
}
}
catch let jsonErr{
print(jsonErr)
}
}.resume()
}
}
I assume the problem is at:
DispatchQueue.main.async{
completed(self.dataArray)
}
So I would like to return the array back to the correct class once it fetched the data from the API. What could I have done wrong? Any help would be much appreciated.
The error is clear: You are using the (wrong) API for appending a single element
Replace
dataArray.append(responseArray)
with
self.dataArray.append(contentsOf: responseArray)
Side note:
Setting and later appending the items again makes no sense. Use a local variable.
Replace
self.dataArray = [try JSONDecoder().decode(ResponseData.self, from: data)]
//Complete task in background
DispatchQueue.main.async {
completed(self.dataArray)
}
with (a different name makes it clearer)
let result = try JSONDecoder().decode(ResponseData.self, from: data)
//Complete task in background
DispatchQueue.main.async {
completed([result])
}

URLSession results in NIL data

I'm trying to learn Swift, and I have a little project with Google's places API.
I have a method for fetching places details, which uses URLSession in swift to send the request:
func fetchRestaurantDetails(placeId: String) -> Void {
let jsonURLString = "https://maps.googleapis.com/maps/api/place/details/json?placeid=\(placeId)&key=[MY API KEY]"
guard let url = URL(string: jsonURLString) else { return}
let urlRequest = URLRequest(url: url)
// set up the session
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
_ = session.dataTask(with: urlRequest) { (data, response, error) in
// check for any errors
guard error == nil else {
print("error calling GET on /todos/1")
print(error!)
return
}
// make sure we got data
guard let responseData = data else {
print("Error: did not receive data")
return
}
// parse the result as JSON, since that's what the API provides
do {
let place = try JSONDecoder().decode(Result.self, from: responseData) // New in Swift 4, used to serialize json.
self.rest = place.result
} catch {
print("error trying to convert data to JSON")
return
}
}.resume()
}
I use this method to create a instance of type Restaurants, which I will later add to a list:
func createRestaurant(placeId: String) -> Restaurants {
self.fetchRestaurantDetails(placeId: placeId)
let rest = Restaurants(name: self.rest.name,
formatted_address: self.rest.formatted_address,
website: self.rest.website,
location: ((self.rest.geometry.location.lat,self.rest.geometry.location.lng)),
opening_hours: self.rest.opening_hours.weekday_text,
photo: restImg)
return rest!
}
But whenever I reach back into the "let rest = Restaurants(...)" all the values are nil. When I try to debug it, it just jumps over my "_ = session" sections right down to resume(), then back to session again and ends back at resume(). No data produced.
I'm quite puzzled since I successfully executed this piece of code before, and now I'm wondering if I missed something.
Thx :-)
Put two breakpoints. One at
let place = try JSONDecoder().decode(Result.self, from: responseData) // New in Swift 4, used to serialize json.
self.rest = place.result
and the second one at
let rest = Restaurants(name: self.rest.name,
formatted_address: self.rest.formatted_address,
website: self.rest.website,
location: ((self.rest.geometry.location.lat,self.rest.geometry.location.lng)),
opening_hours: self.rest.opening_hours.weekday_text,
photo: restImg)
You will realise that the second one is getting called first.
You are fetching data, which is done asynchronously, and before its available you are trying to use it. You need to make sure that the data is available before you use it. One way here would be to use completion handler. You can learn about completion handlers here.
fetchRestaurantDetails is an asynchronous method due to the fact that you call session.dataTask in it, which is asynchronous.
You are trying to use the results of the function before it actually returned. You have several ways to solve this issue:
Use a completion handler to return the value from fetchRestaurantDetails
Use DispatchGroups to detect when the URLRequest finished
Use a 3rd party framework like PromiseKit to handle the asynchronous functions like normal functions with return values.

Resources