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?
tera
2
Something like this will do.
func load(urls: [URL], completion: @escaping (Result<[Data], Error>) -> Void) {
var datas = [Data](repeating: Data(), count: urls.count)
var completed = 0
var hadError = false
let session = URLSession(configuration: .ephemeral)
let tasks = urls.enumerated().map { index, url in
session.dataTask(with: url) { data, response, error in
if let error = error ?? response?.error {
if !hadError {
hadError = true
completion(.failure(error))
session.invalidateAndCancel()
}
} else {
datas[index] = data!
completed += 1
if completed == urls.count {
completion(.success(datas))
}
}
}
}
tasks.forEach { $0.resume() }
}
Note that URLResponse might have an error.
Also, normally JSON decoding can be done on a background queue.
2 Likes
Hi, tera. Thank you very much for a kind reply!
It was really helpful, so I succesfully adapted your code for my needs.
Also, normally JSON decoding can be done on a background queue.
I am a new to SWIFT, so in my case my code updates the UI straight after JSON parsing. I've read somewhere that if you' ll need parsed data somewhere in the future - then use a background queue, if not - better to wrap it in a main thread since you interact with a UI.
Am I understood it correct or was wrong?
tera
4
You may decode JSON on a background queue and then update UI on the main queue. Pseudocode:
load(urls) { resut in
dispatchPrecondition(.notOnQueue(.main))
switch result {
case .success(datas):
let a = datas[0].decodeJson(A.self)
let b = datas[1].decodeJson(B.self)
DispatchQueue.main.async {
// assuming a & b are published properties of your model object:
model.a = a
model.b = b
}
}
}
3 Likes
Got it. Thank you very much!