Variables goes nil after execution of block - swift

I'm having difficulty assigning a value after execution of a closure.
The problem is in //step4. It prints the contents and it captures no nil,
but at //step5 it prints Nil.
I tried to creating a class and assigning the values within the HTTP Request, but no change.
My code:
var x: String!override func viewDidLoad() {
super.viewDidLoad()
//1
let urlAsString = "http://date.jsontest.com/"
let url = NSURL(string: urlAsString)!let urlSession = NSURLSession.sharedSession()
//2
let jsonQuery = urlSession.dataTaskWithURL(url, completionHandler: {
data, response, error - > Void in
if error != nil {
println(error.localizedDescription)
}
var err: NSError ?
//3
var jsonResult = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: & err) as!NSDictionary
if err != nil {
println("JSON Error \(err!.localizedDescription)")
}
//4
let jsonDate = jsonResult["date"] as!String
let jsonTime = jsonResult["time"] as!String
//Step 4
//let's Assume
x = jsonDate
println("Inside \(x)") // Prints Date :)
dispatch_async(dispatch_get_main_queue(), {
self.dateLabel.text = jsonDate
self.timeLabel.text = jsonTime
})
}) //end of JsonQuery
jsonQuery.resume()
//Step 5
println("Outside \(x)") // Prints nil
}

Your web request is an asynchronous request. That means it will finish, and the completion block will be called, at some point in the future. Some time long after you call jsonQuery.resume(). Some time long after your whole function returns.
Your variable doesn't go back to nil after the call - you check it long before it is ever set to nil. It is the job of your completion block to do everything that is necessary to process the data and store the results.

Your "step 5" will likely execute before steps 3 or 4. Calling resume does not block and wait for the response to compete so you execute step 5 before the completion block is called.

Related

How to check if one of URLSession tasks returned an error and if so to stop code execution?

I need to make 2 API calls simultaneously. I have 2 URLs for the calls, and if one of the calls will return any error I want to stop all the code execution.
How I tried to do it:
I have a function called performRequest() with a completion block. I call the function in my ViewController to update the UI - show an error/or a new data if all was successful. Inside it I create a URLSession tasks and then parse JSON:
I created an array with 2 urls:
func performRequest(_ completion: #escaping (Int?) -> Void) {
var urlArray = [URL]()
guard let urlOne = URL(string: "https://api.exchangerate.host/latest?base=EUR&places=9&v=1") else { return }
guard let urlTwo = URL(string: "https://api.exchangerate.host/2022-05-21?base=EUR&places=9") else { return }
urlArray.append(urlOne)
urlArray.append(urlTwo)
}
Then for each of the url inside the array I create a session and a task:
urlArray.forEach { url in
let session = URLSession(configuration: .ephemeral)
let task = session.dataTask(with: url) { data, _, error in
if error != nil {
guard let error = error as NSError? else { return }
completion(error.code)
return
}
if let data = data {
let printData = String(data: data, encoding: String.Encoding.utf8)
print(printData!)
DispatchQueue.main.async {
self.parseJSON(with: data)
}
}
}
task.resume()
}
print("all completed")
completion(nil)
}
For now I receive print("all completed") printed once in any situation: if both tasks were ok, if one of them was ok or none of them.
What I want is to show the print statement only if all tasks were completed successfully and to stop executing the code if one of them returned with error (for example if we will just delete one of the symbols in url string which will take it impossible to receive a data).
How can I do it correctly?

swift - order of functions - which code runs when?

I have an issue with my code and I think it could be related to the order in which code is called.
import WatchKit
import Foundation
class InterfaceController: WKInterfaceController {
private var tasks = [Task]()
override func willActivate() {
let taskUrl = "http://myjsonurl.com"
downloadJsonTask(url: taskUrl)
print(tasks.count) // EMPTY
super.willActivate()
}
func downloadJsonTask(url: String) {
var request = URLRequest(url: URL(string: url)!)
request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
URLSession.shared.dataTask(with: request) { data, urlResponse, error in
guard let data = data, error == nil, urlResponse != nil else {
print("something is wrong")
return
}
do
{
let decoder = JSONDecoder()
let downloadedTasks = try decoder.decode(Tasks.self, from: data)
self.tasks = downloadedTasks.tasks
print(downloadedTasks.tasks.count) //4
} catch {
print("somehting went wrong after downloading")
}
}.resume()
}
}
I'm defining the private var tasks and fill it with the downloadJsonTask function but after the function ran the print(tasks.count) gives 0.
When I call print(downloadedTasks.tasks.count) it gives 4.
I think that in sequence of time the tasks variable is empty when I print it and it is filled later on.
When you are trying to print number of tasks in willActivate(), function downloadJsonTask(url: String) hasn't been completed yet, so you have empty array because tasks haven't been set yet.
You should add completion handler to downloadJsonTask just like this:
(don't forget to pass completion as parameter of function)
func downloadJsonTask(url: String, completion: #escaping () -> Void) {
var request = URLRequest(url: URL(string: url)!)
request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
URLSession.shared.dataTask(with: request) { data, urlResponse, error in
guard let data = data, error == nil, urlResponse != nil else {
print("something is wrong")
completion()
return
}
do {
let decoder = JSONDecoder()
let downloadedTasks = try decoder.decode(Tasks.self, from: data)
self.tasks = downloadedTasks.tasks
print(downloadedTasks.tasks.count) //4
} catch {
print("something went wrong after downloading")
}
completion() // This is moment when code which you write inside closure get executed
}.resume()
}
In your willActivate() use this function like this:
downloadJsonTask(url: taskUrl) {
print(tasks.count)
}
So that means when you get your data, your code inside curly braces will get executed.
You’re correct in your assumption that tasks has not yet been assigned a value when it’s first printed.
The thing is network requests are performed asynchronously. It means that iOS does not wait until downloadJsonTask(url:) is finished but continues executing the code right away (i.e. it calls print(tasks.count) immediately after the network request started, without waiting for it to produce any results).
The piece of code inside brackets after URLSession.shared.dataTask(with:) is called a completion handler. This code gets executed once the network request is competed (hence the name). The tasks variable is assigned a value only when the request is finished. You can make sure it works by adding print(self.tasks.count) after self.tasks = downloadedTasks.tasks:
self.tasks = downloadedTasks.tasks
print(self.tasks)
print(downloadedTasks.tasks.count)

function return too early

Can anyone explain why the completion returns empty array?
The function:
import Foundation
class IMBD{
func searchMovies(searchText:String, completion: (result: [Movies]) -> Void){
var movies = [Movies]()
let replacedMovieTitle = searchText.stringByReplacingOccurrencesOfString(" ", withString: "+")
let URLString = "http://www.omdbapi.com/?s=\(replacedMovieTitle)&y=&r=json"
let URL = NSURL(string: URLString)
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithURL(URL!, completionHandler: {(data, response, error) -> Void in
do{
let jsonData = try NSJSONSerialization.JSONObjectWithData(data!, options: .MutableContainers) as! NSDictionary
if let search = jsonData["Search"] as? [[String : AnyObject]]{
for hit in search{
guard let title = hit["Title"] as? String else{
print("returna title")
return
}
guard let year = hit["Year"] as? String else{
print("returna year")
return
}
guard let imbdID = hit["imdbID"] as? String else{
print("returna imbd")
return
}
guard let poster = hit["Poster"] as? String else{
print("returna poster")
return
}
let movie = Movies(title: title, released: year, poster: poster, imbdID: imbdID)
movies.append(movie)
}
}
}catch{
}
}).resume()
completion(result: movies)
}
}
The call:
imbd.searchMovies(searchtext!, completion: { (result) -> Void in
self.movieList = result
})
You have to call your completion handles inside the dataTaskWithURL closure, not after it. This runs asynchronously, so if you call your completion outside of the closure, it would be called before the asynchronous request had a chance to retrieve anything.
Also, remember that this closure doesn't run on the main thread, so you likely want to also dispatch this to the main queue (from within the dataTaskWithURL).
For example:
class IMDB {
func searchMovies(searchText:String, completion: (result: [Movie]?, error: NSError?) -> Void) -> NSURLSessionTask {
var movies = [Movie]()
let allowedCharacters = NSCharacterSet.alphanumericCharacterSet().mutableCopy() as! NSMutableCharacterSet
allowedCharacters.addCharactersInString("-._* ")
let replacedMovieTitle = searchText.stringByAddingPercentEncodingWithAllowedCharacters(allowedCharacters)!
.stringByReplacingOccurrencesOfString(" ", withString: "+")
let URLString = "http://www.omdbapi.com/?s=\(replacedMovieTitle)&y=&r=json"
let URL = NSURL(string: URLString)
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithURL(URL!) { data, response, error in
guard error == nil && data != nil else {
dispatch_async(dispatch_get_main_queue()) {
completion(result: nil, error: error)
}
return
}
do {
let jsonData = try NSJSONSerialization.JSONObjectWithData(data!, options: .MutableContainers) as! NSDictionary
if let search = jsonData["Search"] as? [[String : AnyObject]]{
for hit in search{
guard let title = hit["Title"] as? String else{
print("returna title")
continue
}
guard let year = hit["Year"] as? String else{
print("returna year")
continue
}
guard let imdbID = hit["imdbID"] as? String else{
print("returna imbd")
continue
}
guard let poster = hit["Poster"] as? String else{
print("returna poster")
continue
}
let movie = Movie(title: title, released: year, poster: poster, imdbID: imdbID)
movies.append(movie)
}
}
dispatch_async(dispatch_get_main_queue()) {
completion(result: movies, error: nil)
}
} catch let error as NSError {
dispatch_async(dispatch_get_main_queue()) {
completion(result: nil, error: error)
}
}
}
task.resume()
return task
}
}
A couple of other changes in the above code snippet include:
Add guard in case there was a fundamental network error (e.g. remote server down, no Internet access, etc.)
In the guard statements that are checking for nil values, rather than performing a return (in which case no further results will be gathered), you might want to just continue (i.e. skip to the next record). You generally see guard in conjunction with return, but in this case, continue is probably more appropriate.
Frankly, you might want to take this a step further and consider whether some of these might be optional, rather than discarding the whole record. Notably, poster strikes me as something that might be nil if there was no poster available. Maybe some of the others should be optional, too, (e.g. if a movie hasn't been released yet, might it not have a release date?).
The occurrences of "imbd" have been replaced with "imdb".
The Movies class has been renamed to Movie (since each instance is a single movie, not a collection of them).
I changed the completion block to make [Movie] optional and to return the NSError. Without that, you don't have a way to differentiate between "couldn't find a title of that name" and "whoops, something went wrong".
When we call the completion closure from within the dataTaskWithURL, it can be very useful to have searchMovies dispatch completion calls back to the main queue, like above. This is because UI updates must always happen on the main thread, and frequently when you write routines like this, it is so you can update UI or the model with results.
This is not always necessary to do it like this (you might want to just have this call completion directly from the background thread and let the routine that called searchMovies manually dispatch stuff to the main thread itself), but I often find it useful to have this search method just dispatch the completion back to the main thread and be done with it.
As a matter of practice, I always return the NSURLSessionTask when performing requests. You might not need it now, but at some future date, you might want the ability to cancel an on-going request, and having a reference to the task can be useful. It doesn't hurt to return it, and it can be useful.
You probably should be percent escaping the values you add to the URL. Notably the presence of & or + characters could be problematic. Note, in this case, it looks like this site isn't handling it appropriately, anyway, but it's good to get in the habit of properly percent-escaping values in a query.
Personally, I keep this percent escaping logic in a String extension, but I wanted to keep this simple, so I embedded it right in this method, but hopefully it illustrates the idea.

The variable doesn't change outside of the closure

var arrayLength:Int = 0 // is equal to 0
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let urlPath = "http://example.com/json"
let url = NSURL(string: urlPath)!
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithURL(url, completionHandler: {
data, response, error in
if (error? != nil) {
println(error)
} else {
var jsonResponse: AnyObject? = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: nil)
let json = JSON(jsonResponse!)
self.arrayLength = json["dump"].array?.count as Int!
println("Second: \(self.arrayLength)") // is equal to 3
for postIndex in 0...self.arrayLength-1 {
println(json["dump"][postIndex]["title"])
}
}
})
task.resume()
println(arrayLength) // is equal to 0 again
I set the
var arrayLength:Int = 0
in the beginning of code. Later inside of the viewDidLoad() I changed it to 3. And when I call it, outside of the task block it again equal to 0. What is the problem here and what I do wrong?
Yes, you're changing your arrayLengthvar inside your closure. The problem you're facing here it's just normal way of how threads work.
When viewDidLoadstarts you're on the Main Thread
the line let task = session.dataTaskWithURL(url, completionHandler: { ... defines a new task, that's not yet executing
task.resume() launches this task on another thread. This is going to take some time to finish. When it's finished your arrayLength will be changed
immediately you're asking on the main thread for your arrayLength value
Here:
println(arrayLength) // is equal to 0 again
it's not that arrayLength is equal to 0 again, you haven't given enough time to your task to execute and change that value.
EDIT: if you need to check arrayLength after the closure finishes...
Just create a function and call it last thing inside your closure. Like this:
override func viewDidLoad() {
...
let task = session.dataTaskWithURL(url, completionHandler: {
data, response, error in
if (error? != nil) {
println(error)
} else {
var jsonResponse: AnyObject? = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: nil)
let json = JSON(jsonResponse!)
self.arrayLength = json["dump"].array?.count as Int!
println("Second: \(self.arrayLength)") // is equal to 3
for postIndex in 0...self.arrayLength-1 {
println(json["dump"][postIndex]["title"])
}
functionToDoSomethingAtTheEndOfTheClosure();
}
})
task.resume()
}
func functionToDoSomethingAtTheEndOfTheClosure() {
// this is going to be executed at the end of the closure's code
}

Clousure code won't execute wrapped in a function

I have a closure making a http call wrapped in a function which is called from the click of a button. However when I debug I can see the code within the closure never executes, the programme jumps out of the function altogether when it reaches the closure.
func getTheForeCast(city: String) {
println("Function getForecast city passed = : \(city)")
var webAddress: String = "http://www.weather-forecast.com/locations/\(city)/forecasts/latest"
println("Web address url : \(webAddress)")
let url = NSURL(string: webAddress)
println(url!)
// PROGRAM EXITS FUNCTION HERE
let openbrowserSession = NSURLSession.sharedSession().dataTaskWithURL(url!) {
(data, response, error) in
// in the following code, session returns data, error, and response
println("In closure")
if error == nil {
// no errors, convert html to readable data
var urlConverted = NSString(data: data, encoding: NSUTF8StringEncoding)
println(urlConverted)
// run this asynchronously using a grand central dispatch
dispatch_async(dispatch_get_main_queue()) { self.webview_displayWeather.loadHTMLString(urlConverted, baseURL: nil) } // dispatch
} else if error != nil {
println("Error loading page")
println(error.description)
}
} // closure
} // func
Any input appreciated.
The tasks created by NSURLSession are initially in the "suspended" state.
You have to call resume() after creating the task:
let openbrowserSession = NSURLSession.sharedSession().dataTaskWithURL(url!) {
(data, response, error) in
// ...
}
openbrowserSession.resume()
otherwise nothing will happen.
You use the wrong signature. Use
func dataTaskWithURL(_ url: NSURL,
completionHandler completionHandler: ((NSData!,
NSURLResponse!,
NSError!) -> Void)?) -> NSURLSessionDataTask