DispatchGroup not working with URLSession.shared.dataTask - ios

I'm trying to implement many concurrent api calls through URLSession.shared singleton. What I want is that they are made concurrently so when I had all the responses, then execute the completion handler (I took this as example: https://medium.com/#oleary.audio/simultaneous-asynchronous-calls-in-swift-9c1f5fd3ea32):
init(completion: (()->Void)? = nil) throws {
self.myToken = myToken
let group = DispatchGroup()
group.enter()
print("Downloading User")
self.downloadUser(completion: { userObject in
self.userObject = userObject
group.leave()
})
group.enter()
print("Downloading Tickets")
self.downloadTickets(completion: { ticketsObject in
if let ticketsObject = ticketsObject {
self.ticketsObject = ticketsObject
}
group.leave()
})
group.wait()
completion?()
}
Then the functions the implement the api calls are something like:
private func downloadUser( completion: #escaping ((myUsuario)->Void )) {
let url = URL(string: "\(Globals.laravelAPI)me")
var request = URLRequest(url: url!)
let task = URLSession.shared.dataTask(with: request) {
(data:Data?, response:URLResponse?, error:Error?) in
if let error = error {
...
}
guard let httpResponse = response as? HTTPURLResponse,(200...299).contains(httpResponse.statusCode) else { ... }
if let data = data {
do {
let user = try JSONDecoder().decode(myUsuario.self, from: data)
completion(user)
} catch {
print("USER PARSING ERROR: \(error)")
fatalError()
}
}
}
task.resume()
}
When I run the program, it make the calls but never get the responses so the group.wait() is never executed.

Related

Swift completion handlers - using escaped closure?

Hi i am a beginner studying swift and would like to know what to use when making an api request. What is the modern and professional way?
is it using an escaping closure like so:
func getTrendingMovies(completion: #escaping (Result< [Movie], Error >) -> Void) {
guard let url = URL(string: "\(Constants.baseUrl)/trending/all/day?api_key=\.(Constants.API_KEY)") else {return}
let task = URLSession.shared.dataTask(with: URLRequest(url: url)) { data, _,
error in
guard let data = data, error == nil else {
return
}
do {
let results = try JSONDecoder().decode(TrendingMoviesResponse.self, from:
data)
completion(.success(results.results))
} catch {
completion(.failure(error))
}
}
task.resume()
}
or should i make an api request without escaping closure while using a sort of delegate like so:
func performRequest(with urlString: String){
if let url = URL(string: urlString){
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { data, response, error in
if error != nil {
delegate?.didFailWithError(error: error!)
return
}
if let safeData = data{
// created parseJson func
if let weather = parseJSON(safeData){
delegate?.didUpdateWeather(self,weather: weather)
}
}
}
task.resume()
} else {
print("url is nil")
}
}
I agree with matt, the modern and professional way is async/await
func getTrendingMovies() async throws -> [Movie] {
let url = URL(string: "\(Constants.baseUrl)/trending/all/day?api_key=\(Constants.API_KEY)")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(TrendingMoviesResponse.self, from: data).results
}

URLSession does not call API. Though in Playground it works

So this code works for me in playground but for some reason URLSession.shared.dataTask(... doesnt call my flask api that im currently locally running. Any idea on what's wrong? So far I'm only concerned on why it does not enter the do{in my project but it works properly in playground.
func getWords() -> [Word]{
var words = [Word]()
let url = URL(string: self.url)
let request = URLRequest(url: url!)
let group = DispatchGroup()
print("XD")
URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) in
do {
print("A")
if let data = data{
print("B")
if let decodedResponse = try? JSONDecoder().decode([Word].self, from: data){
group.enter()
DispatchQueue.main.async(){
words = decodedResponse
print("C")
print(words)
group.leave()
}
}
}
print("DD")
} catch {
print("Words.swift Error in try catch")
}
group.enter()
}).resume()
group.leave()
group.notify(queue: DispatchQueue.main, execute: {
print(words)
})
print("ASDASD WORDS: \(words)")
for _ in 1 ... 4 {
// - to make sure there aren't duplicates -
var wordId:Int = Int.random(in: 0..<words.count)
while randomIds.contains(wordId){
wordId = Int.random(in: 0..<words.count)
}
randomIds.append(wordId)
}
//returns 4 words
return words
}
You aren't using DispatchGroup correctly; You should call enter before you start the asynchronous work and leave once it is complete. You can then use notify to perform some operation.
However, you don't really need a DispatchGroup in this situation; You have that because you are trying to turn an asynchronous operation into a synchronous one;
The correct approach is to accept that the operation is asynchronous and it isn't possible for this function to return [Word]. You will need to refactor the function to accept a completion handler closure and invoke that with the result.
Something like this:
func getWords(completionHandler:#escaping (Result<[Word], Error>) -> Void) -> Void{
var words = [Word]()
let url = URL(string: self.url)
let request = URLRequest(url: url!) // Note you should use a guard and call the completion handler with an error if url is `nil`
URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) in
if let error = error {
completionHandler(.failure(error))
} else {
do {
if let data = data {
let words = try JSONDecoder().decode([Word].self, from: data)
completionHandler(.success(words))
} else {
// TODO call completionHander with a .failure(SomeError)
}
} catch {
completionHandler(.failure(error))
}
}
}).resume()
}
Then you can call it:
getWords() { result in
switch result {
case .success(let words):
print(words)
case .failure(let error):
print(error.localizedDescription)
}
}

Why are dispatch groups not waiting?

I'm reading about GCD lately and trying to implement a series of network calls using the DispatchGroup, however, I'm am not seeing the desired results. If my understanding is correct whenever we use wait on the dispatch group it should block the thread until all enter and leaves are equal.In my case it not blocking the thread. Here is my piece of code
DispatchQueue.global(qos: .userInitiated).async {
let dispatchGroup = DispatchGroup()
print("herer \(Thread.current)")
var userAvatorsToLoad: [String] = []
let url = URL(string:"https://api.github.com/users")!
URLSession.shared.dataTask(with: url) { (data, response, error) in
if let response = response as? HTTPURLResponse , response.statusCode == 200 {
print("her\(Thread.current)")
let userResponse = try? JSONDecoder().decode([Users].self, from: data!)
userAvatorsToLoad.append(contentsOf:[userResponse![0].avatar_url,
userResponse![1].avatar_url,
userResponse![2].avatar_url])
userAvatorsToLoad.forEach {[weak self] (imageUrl) in
dispatchGroup.enter()
self?.loadImage(url: imageUrl) {
print("image Successfully cached")
dispatchGroup.leave()
}
}
}
}.resume()
dispatchGroup.wait()
print("hello")
}
In the output, I'm seeing hello even before any of my async operations are performed. Am i missing anything
The problem is that by the time you hit the wait call, it hadn’t yet encountered the forEach loop which was performing the enter and leave calls.
If you’re going to wait for an asynchronous request which is using the enter/leave calls, you’ll need to add a enter/leave for the main asynchronous request as well.
DispatchQueue.global(qos: .userInitiated).async {
let group = DispatchGroup()
var userAvatorsToLoad: [String] = []
let url = URL(string:"https://api.github.com/users")!
group.enter() // ADD THIS ...
URLSession.shared.dataTask(with: url) { (data, response, error) in
defer { group.leave() } // ... AND THIS
if let response = response as? HTTPURLResponse , response.statusCode == 200 {
print("her\(Thread.current)")
let userResponse = try? JSONDecoder().decode([Users].self, from: data!)
userAvatorsToLoad.append(contentsOf:[userResponse![0].avatar_url,
userResponse![1].avatar_url,
userResponse![2].avatar_url])
userAvatorsToLoad.forEach {[weak self] (imageUrl) in
group.enter()
self?.loadImage(url: imageUrl) {
print("image Successfully cached")
group.leave()
}
}
}
}.resume()
group.wait()
print("hello")
}
But, you should generally avoid wait. Use notify.
let url = URL(string:"https://api.github.com/users")!
URLSession.shared.dataTask(with: url) { data, response, error in
guard
let response = response as? HTTPURLResponse,
200 ..< 300 ~= response.statusCode,
let data = data,
let userResponse = try? JSONDecoder().decode([Users].self, from: data)
else {
return
}
let userAvatorsToLoad = [
userResponse[0].avatar_url,
userResponse[1].avatar_url,
userResponse[2].avatar_url
]
let group = DispatchGroup()
userAvatorsToLoad.forEach { imageUrl in
group.enter()
self.loadImage(url: imageUrl) {
print("image Successfully cached")
group.leave()
}
}
group.notify(queue: .main) {
print("Hello")
}
}.resume()
This avoids blocking one of the very limited GCD worker threads while you perform the request. It also completely eliminates the need for the global concurrent queue at all.
You have to move "dispatchGroup.enter()" before the "URLSession.shared.dataTask" methods will be called.
So the code will be:
DispatchQueue.global(qos: .userInitiated).async {
let dispatchGroup = DispatchGroup()
print("herer \(Thread.current)")
var userAvatorsToLoad: [String] = []
let url = URL(string:"https://api.github.com/users")!
dispatchGroup.enter()
URLSession.shared.dataTask(with: url) { (data, response, error) in
if let response = response as? HTTPURLResponse , response.statusCode == 200 {
print("her\(Thread.current)")
let userResponse = try? JSONDecoder().decode([Users].self, from: data!)
userAvatorsToLoad.append(contentsOf:[userResponse![0].avatar_url,
userResponse![1].avatar_url,
userResponse![2].avatar_url])
userAvatorsToLoad.forEach {[weak self] (imageUrl) in
dispatchGroup.enter()
self?.loadImage(url: imageUrl) {
print("image Successfully cached")
dispatchGroup.leave()
}
}
}
dispatchGroup.leave()
}.resume()
dispatchGroup.wait()
print("hello")
}

How can I unit test a network request using a local json file?

I'm trying to figure out the best way to unit test a network request. My initial thought was to create a local file with the JSON response for testing purposes but that doesn't seem to be working. See my code below.
I wanna test that I can get a non-nil array back from the completion handler in the function below.
class APIClient {
let downloader = JSONDownloader() // just a class that creates a new data task
// what I want to test
func getArticles(from url: URL?, completion: #escaping([Article]?, Error?) -> ()) {
guard let url = url else { return }
let request = URLRequest(url: url)
let task = downloader.createTask(with: request) { json, error in
DispatchQueue.main.async {
// parse JSON
...
completion(articles, nil)
}
}
task.resume()
}
}
I tried testing as shown below to no avail.
func testArticleResponseIsNotNil() {
let bundle = Bundle(for: APIClientTests.self)
guard let path = Bundle.path(forResource: "response-articles", ofType: "json", inDirectory: bundle.bundlePath) else {
XCTFail("Missing file: response-articles.json")
return
}
let url = URL(fileURLWithPath: path)
var articles: [Article]?
let expectation = self.expectation(description: "Articles")
let client = APIClient()
client.getArticles(from: url) { response, error in
articles = response
expectation.fulfill()
}
wait(for: [expectation], timeout: 5)
XCTAssertNotNil(articles)
}
Any ideas on how exactly I should test this function?
Edit: This is the JSONDownloader class.
class JSONDownloader {
let session: URLSession
init(configuration: URLSessionConfiguration) {
self.session = URLSession(configuration: configuration)
}
convenience init() {
self.init(configuration: .default)
}
typealias JSON = [String: AnyObject]
func createTask(with request: URLRequest, completion: #escaping(JSON?, Error?) -> ()) -> URLSessionDataTask {
let task = session.dataTask(with: request) { data, response, error in
guard let httpResponse = response as? HTTPURLResponse else { return }
if httpResponse.statusCode == 200 {
if let data = data {
do {
let json = try JSONSerialization.jsonObject(with: data, options: []) as? JSON
completion(json, nil)
} catch { completion(nil, error) }
} else { completion(nil, error) }
} else { completion(nil, error) }
}
return task
}
}

Determine when urlsession.shared and Json parsing are finished

I am downloading and then reading a json file. this json contains a list of files and their address on the server.
Everything works fine but I want to get the size of all files to download.
but I have some trouble to set up a completionblock that would indicate that everything is finished.
here is the code.
jsonAnalysis {
self.sum = self.sizeArray.reduce(0, +)
print(self.sum)
} here
func jsonAnalysis(completion: #escaping () -> ()) {
let urlString = "xxxxxxxxxxxxxxxxxxxxx"
let url = URL(string: urlString)
URLSession.shared.dataTask(with:url!) { (data, response, error) in
if error != nil {
print("error")
} else {
do {
let json = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? [String: Any]
self.i = -1
guard let array = json?["Document"] as? [Any] else { return }
for documents in array {
self.i = self.i + 1
guard let VersionDictionary = documents as? [String: Any] else { return }
guard let DocumentName = VersionDictionary["documentname"] as? String else { return }
guard let AddressServer = VersionDictionary["addressserver"] as? String else { return }
self.resultAddressServer.append(AddressServer)
self.addressServer = self.resultAddressServer[self.i]
self.resultDocumentName.append(DocumentName)
self.documentName = self.resultDocumentName[self.i]
let url1 = NSURL(string: AddressServer)
self.getDownloadSize(url: url1! as URL, completion: { (size, error) in
if error != nil {
print("An error occurred when retrieving the download size: \(String(describing: error?.localizedDescription))")
} else {
self.sizeArray.append(size)
print(DocumentName)
print("The download size is \(size).")
}
})
}
} catch {
print("error")
}
}
completion()
} .resume()
}
func getDownloadSize(url: URL, completion: #escaping (Int64, Error?) -> Void) {
let timeoutInterval = 5.0
var request = URLRequest(url: url,
cachePolicy: .reloadIgnoringLocalAndRemoteCacheData,
timeoutInterval: timeoutInterval)
request.httpMethod = "HEAD"
URLSession.shared.dataTask(with: request) { (data, response, error) in
let contentLength = response?.expectedContentLength ?? NSURLSessionTransferSizeUnknown
completion(contentLength, error)
}.resume()
}
I would like to get the sum of the array at the end when everything is done, right now print(self.sum) is running before and shows 0.
I am not familiar with the completion and I am sure I am doing everything wrong.
You need DispatchGroup.
Before calling the inner asynchronous task enter, in the completion block of the inner asynchronous task leave the group.
Finally when the group notifies, call completion
let group = DispatchGroup()
for documents in array {
...
let url1 = URL(string: AddressServer) // no NSURL !!!
group.enter()
self.getDownloadSize(url: url1!, completion: { (size, error) in
if error != nil {
print("An error occurred when retrieving the download size: \(String(describing: error?.localizedDescription))")
} else {
self.sizeArray.append(size)
print(DocumentName)
print("The download size is \(size).")
}
group.leave()
})
}
group.notify(queue: DispatchQueue.main) {
completion()
}

Resources