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
Related
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?
I am trying to recover a data set from a URL (after parsing a JSON through the parseJSON function which works correctly - I'm not attaching it in the snippet below).
The outcome returns nil - I believe it's because the closure in retrieveData function is processed asynchronously. I can't manage to have the outcome saved into targetData.
Thanks in advance for your help.
class MyClass {
var targetData:Download?
func triggerEvaluation() {
retrieveData(url: "myurl.com") { downloadedData in
self.targetData = downloadedData
}
print(targetData) // <---- Here is where I get "nil"!
}
func retrieveData(url: String, completion: #escaping (Download) -> ()) {
let myURL = URL(url)!
let mySession = URLSession(configuration: .default)
let task = mySession.dataTask(with: myURL) { [self] (data, response, error) in
if error == nil {
if let fetchedData = data {
let safeData = parseJSON(data: fetchedData)
completion(safeData)
}
} else {
//
}
}
task.resume()
}
}
Yes, it’s nil because retrieveData runs asynchronously, i.e. the data hasn’t been retrieved by the time you hit the print statement. Move the print statement (and, presumably, all of the updating of your UI) inside the closure, right where you set self.targetData).
E.g.
func retrieveData(from urlString: String, completion: #escaping (Result<Download, Error>) -> Void) {
let url = URL(urlString)!
let mySession = URLSession.shared
let task = mySession.dataTask(with: url) { [self] data, response, error in
guard
let responseData = data,
error == nil,
let httpResponse = response as? HTTPURLResponse,
200 ..< 300 ~= httpResponse.statusCode
else {
DispatchQueue.main.async {
completion(.failure(error ?? NetworkError.unknown(response, data))
}
return
}
let safeData = parseJSON(data: responseData)
DispatchQueue.main.async {
completion(.success(safeData))
}
}
task.resume()
}
Where
enum NetworkError: Error {
case unknown(URLResponse?, Data?)
}
Then the caller would:
func triggerEvaluation() {
retrieveData(from: "https://myurl.com") { result in
switch result {
case .failure(let error):
print(error)
// handle error here
case .success(let download):
self.targetData = download
// update the UI here
print(download)
}
}
// but not here
}
A few unrelated observations:
You don't want to create a new URLSession for every request. Create only one and use it for all requests, or just use shared like I did above.
Make sure every path of execution in retrieveData calls the closure. It might not be critical yet, but when we write asynchronous code, we always want to make sure that we call the closure.
To detect errors, I'd suggest the Result pattern, shown above, where it is .success or .failure, but either way you know the closure will be called.
Make sure that model updates and UI updates happen on the main queue. Often, we would have retrieveData dispatch the calling of the closure to the main queue, that way the caller is not encumbered with that. (E.g. this is what libraries like Alamofire do.)
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)
I've created a macOS console app in swift, but the code is never executed, =I have to use Semaphore but is there another way to do this ?
my purpose is to create a method returning a json file
class test{
func gizlo(){
let config = URLSessionConfiguration.default // Session Configuration
let session = URLSession(configuration: config) // Load configuration into Session
let url = URL(string: "https://itunes.apple.com/fr/rss/topmovies/limit=25/json")!
let task = session.dataTask(with: url, completionHandler: {
(data, response, error) in
if error != nil {
print(error!.localizedDescription)
} else {
do {
if let json = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? [String: Any]
{
print(json)
}
} catch {
print("error in JSONSerialization")
}
}
})
task.resume()
}
}
let tr=test()
tr.gizlo()
Thanks
To avoid Semaphores you can use simple readLine() that will wait for input from the keyboard. Yes it is not obvious but it is woking because it prevent terminal app from exit.
Just add in the and of the file:
_ = readLine()
As Oleg points out, putting readLine() at the end of the top-level code will prevent the program for exiting until you hit Enter in the terminal or wherever FileHandle.standardInput is pointing. That's probably fine for just testing the code quickly in the debugger or in a Playground. An infinite loop would also work, though you'd have to actually terminate it in the debugger or with kill from the command line.
The real issue is why you don't want to use a semaphore. Since they're not difficult to use, I'm going to hazard a guess that it's just because you don't want to pollute your asynchronous data task completion handler with a semaphore when you probably only need it to wait for the data for testing purposes.
Assuming my guess is correct, the real issue isn't actually using a semaphore, it's where you think you need to put them. As David Wheeler once said, "Any problem can be solved by adding a layer of indirection."
You don't want the semaphore explicitly in the completion handler you pass to dataTask. So one solution would be to make gizlo accept a completion handler of its own, and then create a method that calls gizlo with a closure that handles the semaphore. That way you can decouple the two and even add some flexibility for other uses. I've modified your code to do that:
import Foundation
import Dispatch // <-- Added - using DispatchSemaphore
class test{
func gizlo(_ completion: ((Result<[String: Any]?, Error>) -> Void)? = nil) { // <-- Added externally provided completion handler
let config = URLSessionConfiguration.default // Session Configuration
let session = URLSession(configuration: config) // Load configuration into Session
let url = URL(string: "https://itunes.apple.com/fr/rss/topmovies/limit=25/json")!
let task = session.dataTask(with: url, completionHandler: {
(data, response, error) in
let result: Result<[String: Any]?, Error>
if let responseError = error { // <-- Changed to optional binding
print(responseError.localizedDescription)
result = .failure(responseError) // <-- Added this
} else {
do {
if let json = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? [String: Any]
{
print(json)
result = .success(json) // <-- Added this
}
else { // <-- Added this else block
result = .success(nil)
}
} catch {
print("error in JSONSerialization")
result = .failure(error) // <-- Added this
}
}
completion?(result) // <-- Added this call
})
task.resume()
}
func blockingGizlo() throws -> [String: Any]? // <-- Added this method
{
let sem = DispatchSemaphore(value: 1)
sem.wait()
var result: Result<[String: Any]?, Error>? = nil
gizlo {
result = $0
sem.signal()
}
sem.wait() // This wait will block until the closure calls signal
sem.signal() // Release the second wait.
switch result
{
case .success(let json) : return json
case .failure(let error) : throw error
case .none: fatalError("Unreachable")
}
}
}
let tr=test()
do {
let json = try tr.blockingGizlo()
print("\(json?.description ?? "nil")")
}
catch { print("Error: \(error.localizedDescription)") }
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.