each time i call this api https://foodish-api.herokuapp.com/api/ i get an image. I don't want one image, i need 11 of them, so i made the loop to get 11 images.
But what i can't do is reloading the collection view once the loop is finish.
func loadImages() {
DispatchQueue.main.async {
for _ in 1...11{
let url = URL(string: "https://foodish-api.herokuapp.com/api/")!
let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
guard let data = data else { return }
do {
let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String : String]
print(json!["image"]!)
self.namesOfimages.append(json!["image"]!)
} catch {
print("JSON error: \(error.localizedDescription)")
}
}.resume()
}
}
self.collectionV.reloadData()
print("after resume")
}
Typically, when we want to know when a series of concurrent tasks (such as these network requests) are done, we would reach for a DispatchGroup. Call enter before the network request, call leave in the completion handler, and specify a notify block, e.g.
/// Load images
///
/// - Parameter completion: Completion handler to return array of URLs. Called on main queue
func loadImages(completion: #escaping ([URL]) -> Void) {
var imageURLs: [Int: URL] = [:] // note, storing results in local variable, avoiding need to synchronize with property
let group = DispatchGroup()
let count = 11
for index in 0..<count {
let url = URL(string: "https://foodish-api.herokuapp.com/api/")!
group.enter()
URLSession.shared.dataTask(with: url) { data, response, error in
defer { group.leave() }
guard let data = data else { return }
do {
let foodImage = try JSONDecoder().decode(FoodImage.self, from: data)
imageURLs[index] = foodImage.url
} catch {
print("JSON error: \(error.localizedDescription)")
}
}.resume()
}
group.notify(queue: .main) {
let sortedURLs = (0..<count).compactMap { imageURLs[$0] }
completion(sortedURLs)
}
}
Personally, rather than JSONSerialization, I use JSONDecoder with a Decodable type to parse the JSON response. (Also, I find the key name, image, to be a bit misleading, so I renamed it to url to avoid confusion, to make it clear it is a URL for the image, not the image itself.) Thus:
struct FoodImage: Decodable {
let url: URL
enum CodingKeys: String, CodingKey {
case url = "image"
}
}
Also note that the above is not updating properties or reloading the collection view. A routine that is performing network requests should not also be updating the model or the UI. I would leave this in the hands of the caller, e.g.,
var imageURLs: [URL]?
override func viewDidLoad() {
super.viewDidLoad()
// caller will update model and UI
loadImages { [weak self] imageURLs in
self?.imageURLs = imageURLs
self?.collectionView.reloadData()
}
}
Note:
The DispatchQueue.main.async is not necessary. These requests already run asynchronously.
Store the temporary results in a local variable. (And because URLSession uses a serial queue, we do not have to worry about further synchronization.)
The dispatch group notify block, though, uses the .main queue, so that the caller can conveniently update properties and UI directly.
Probably obvious, but I am parsing the URL directly, rather than parsing a string and converting that to a URL.
When fetching results concurrently, you have no assurances regarding the order in which they will complete. So, one will often capture the results in some order-independent structure (such as a dictionary) and then sort the results before passing it back.
In this particular case, the order doesn't strictly matter, but I included this sort-before-return pattern in my above example, as it is generally the desired behavior.
Anyway, that yields:
If you want to get one reload after finish loading of all 11 images you need to use DispatchGroup. Add a property that create a group:
private let group = DispatchGroup()
Then modify your loadImages() function:
func loadImages() {
for _ in 1...11 {
let url = URL(string: "https://foodish-api.herokuapp.com/api/")!
group.enter()
URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
guard let self = self else { return }
self.group.leave()
guard let data = data else { return }
do {
let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String : String]
print(json!["image"]!)
self.namesOfimages.append(json!["image"]!)
} catch {
print("JSON error: \(error.localizedDescription)")
}
}.resume()
}
group.notify(queue: .main) { [weak self] in
self?.collectionV.reloadData()
}
}
Some description:
On the method call group.enter() will be called 11 times
On each completion of image downloading group.leave() will be called
When group.leave() will be called the same count like group.enter() group make call of the block that you defined in group.notify()
More about DispatchGroup
Notice that you need handle create and store different DispatchGroup object if you need to download different groups of images in the same time.
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'm trying to use DispatchGroup for fetching data from multiple request.
I cant understand why print(weatherData.fact.pressureMm!) is working, but data didn't appending inside dataArray and print(dataArray?[0].fact.pressureMm ?? "nil") print nil.
Also i'm try print data from complitionHandeler and result was same.
How i can append weatherData inside array and get value from complition correctly?
func fetchWeatherForCities (complitionHandeler: #escaping([YandexWeatherData]?)->Void) {
var dataArray: [YandexWeatherData]?
let group = DispatchGroup()
for city in cities {
group.enter()
DispatchQueue.global().async {
var urlString = self.urlString
self.locationManager.getCoordinate(forCity: city) { (coordinate) in
urlString += self.latitudeField + coordinate.latitude
urlString += self.longitudeField + coordinate.longitude
guard let url = URL(string: urlString) else {return}
var request = URLRequest(url: url)
request.addValue(self.apiKey, forHTTPHeaderField: self.apiField)
let dataTask = URLSession.shared.dataTask(with: request) { (data, response, error) in
if let error = error {
print(error)
}
if let data = data {
guard let weatherData = self.parseJSON(withData: data) else {return}
print(weatherData.fact.pressureMm!)
dataArray?.append(weatherData)
print(dataArray?[0].fact.pressureMm ?? "nil")
group.leave()
}
}
dataTask.resume()
}
}
}
group.notify(queue: DispatchQueue.global()) {
complitionHandeler(dataArray)
}
}
A few issues:
You have paths of execution where, if an error occurred, you would not call leave. Make sure every path of execution, including every “early exit”, offsets the enter with a leave.
You defined dataArray to be an optional, but never initialize it. Thus it is nil. And dataArray?.append(weatherData) therefore will never append values.
Thus, perhaps:
func fetchWeatherForCities (completionHandler: #escaping ([YandexWeatherData]) -> Void) {
var dataArray: [YandexWeatherData] = []
let group = DispatchGroup()
for city in cities {
group.enter()
var urlString = self.urlString
self.locationManager.getCoordinate(forCity: city) { (coordinate) in
urlString += self.latitudeField + coordinate.latitude
urlString += self.longitudeField + coordinate.longitude
guard let url = URL(string: urlString) else {
group.leave() // make sure to `leave` in early exit
return
}
var request = URLRequest(url: url)
request.addValue(self.apiKey, forHTTPHeaderField: self.apiField)
let dataTask = URLSession.shared.dataTask(with: request) { data, response, error in
guard
let data = data,
error == nil,
let weatherData = self.parseJSON(withData: data)
else {
group.leave() // make sure to `leave` in early exit
print(error ?? "unknown error")
return
}
print(weatherData.fact.pressureMm!) // I'd advise against every doing force unwrapping on results from a third party service
dataArray.append(weatherData)
group.leave()
}
dataTask.resume()
}
}
group.notify(queue: .main) {
completionHandler(dataArray)
}
}
As an aside, in the above, I have made two unrelated GCD changes, namely:
Removed the dispatching of the network request to a global queue. Network requests are already asynchronous, so dispatching the creation of the request and the starting of that request is a bit redundant.
In your notify block, you were using a global queue. You certainly can do that if you really need, but most likely you are going to be updating model objects (which requires synchronization if you're doing that from a background queue) and UI updates. Life is easier if you just dispatch that to the main queue.
FWIW, when you get past your current issue, you may want to consider two other things:
If retrieving details for many locations, you might want to constrain this to only run a certain number of requests at a time (and avoid timeouts on the latter ones). One way is to use a non-zero semaphore:
DispatchQueue.global().async {
let semaphore = DispatchSemaphore(value: 4)
for i in ... {
semaphore.wait()
someAsynchronousProcess(...) {
...
semaphore.signal()
}
}
}
If you have used semaphores in the past, this might feel backwards (waiting before signaling; lol), but the non-zero semaphore will let four of them start, and others will start as the prior four individually finish/signal.
Also, because we are now waiting, we have to re-introduce the dispatch to a background queue to avoid blocking.
When running asynchronous requests concurrently, they may not finish in the order that you started them. If you want them in the same order, one solution is to store the results in a dictionary as they finish, and in the notify block, build a sorted array of the results:
var results: [Int: Foo] = [:]
// start all the requests, populating a dictionary with the results
for (index, city) in cities.enumerated() {
group.enter()
someAsynchronousProcess { foo in
results[i] = foo
group.leave()
}
}
// when all done, build an array in the desired order
group.notify(queue: .main) {
let array = self.cities.indices.map { results[$0] } // build sorted array of `[Foo?]`
completionHandler(array)
}
That begs the question about how you want to handle errors, so you might make it an array of optionals (like shown below).
Pulling that together, perhaps:
func fetchWeatherForCities(completionHandler: #escaping ([YandexWeatherData?]) -> Void) {
DispatchQueue.global().async {
var results: [Int: YandexWeatherData] = [:]
let semaphore = DispatchSemaphore(value: 4)
let group = DispatchGroup()
for (index, city) in self.cities.enumerated() {
group.enter()
semaphore.wait()
var urlString = self.urlString
self.locationManager.getCoordinate(forCity: city) { coordinate in
urlString += self.latitudeField + coordinate.latitude
urlString += self.longitudeField + coordinate.longitude
guard let url = URL(string: urlString) else {
semaphore.signal()
group.leave() // make sure to `leave` in early exit
return
}
var request = URLRequest(url: url)
request.addValue(self.apiKey, forHTTPHeaderField: self.apiField)
let dataTask = URLSession.shared.dataTask(with: request) { data, response, error in
defer {
semaphore.signal()
group.leave() // make sure to `leave`, whether successful or not
}
guard
let data = data,
error == nil,
let weatherData = self.parseJSON(withData: data)
else {
print(error ?? "unknown error")
return
}
results[index] = weatherData
}
dataTask.resume()
}
}
group.notify(queue: .main) {
let array = self.cities.indices.map { results[$0] } // build sorted array
completionHandler(array)
}
}
}
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'm getting data from the FatSecret API. In a nutshell, I have several food IDs I need to get data from, iterating over them to add the calories together. Each one must be a separate call. Since these don't run on the main thread, what's the best way of determining when they are all finished? Currently, I track it with a variable that iterates by one each time a call is finished, but I feel like there may be a better way.
for thisFoodId in foodIds {
let endpointURL = <URL WITH FOODID>
guard let url = URL(string: endpointURL) else {
return
}
let urlRequest = URLRequest(url: url)
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
let task = session.dataTask(with: urlRequest, completionHandler: {
(data, response, error) in
guard error == nil else {
return
}
guard let responseData = data else {
return
}
do {
guard let thisData = try JSONSerialization.jsonObject(with: responseData, options: []) as? [String: AnyObject] else {
return
}
let calories = thisData["calories"]
self.totalCalories += calories
tracker += 1
if tracker == foodIds.count {
// RUN COMPLETION CODE
}
}
}
You can use DispatchGroups. It will trigger an async callback block when all your requests are completed.
DispatchGroup:
DispatchGroup allows for aggregate synchronization of work. You can
use them to submit multiple different work items and track when they
all complete, even though they might run on different queues. This
behavior can be helpful when progress can’t be made until all of the
specified tasks are complete.
In your code, it would like this:
// ...
// Create the DispatchGroup:
let dispatchGroup = DispatchGroup()
for thisFoodId in foodIds {
// Enter the DispatchGroup:
dispatchGroup.enter()
// ...
let task = session.dataTask(with: urlRequest, completionHandler: {
(data, response, error) in
// ...
do {
// ...
if tracker == foodIds.count {
// Leave the DispatchGroup
dispatchGroup.leave()
}
}
})
// When all your tasks have completed, notify your DispatchGroup to run your work item:
dispatchGroup.notify(queue: DispatchQueue.main, execute: {
// All your async requests are finished here, do whatever you want :)
})
}
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)") }