1

I use dispatch group in a loop in order to keep track an array of network requests so when they are all successfully completed then the successful completion handler is returned only once.

However, if a single failure occur, I want to abort the entire operation and return the failure completion handler only once. The problem I am facing is that all failure completion handlers are returned multiple times. Which is not what I want.

My code looks something like this.

class NetworkClient {
    // ...
    func fetchBlogPosts(completion: @escaping (Result<[BlogPost], NetworkClientError>) -> Void) {
        let dispatchGroup = DispatchGroup()
        var blogPosts     = [BlogPost]()
        
        for (index, value) in blogPostJSONURLs.enumerated() {
            dispatchGroup.enter()
            
            guard let jsonURL = URL(string: blogPostJSONURLs[index]) else {
                dispatchGroup.leave()
                completion(.failure(.invalidURL))
                return
            }
            
            let dataTask = URLSession.shared.dataTask(with: jsonURL) { data, response, error in
                if error != nil {
                    dispatchGroup.leave()
                    completion(.failure(.errorReturned))
                    return
                }
                
                guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
                    dispatchGroup.leave()
                    completion(.failure(.serverError))
                    return
                }
                
                guard let data = data else {
                    dispatchGroup.leave()
                    completion(.failure(.noData))
                    return
                }
                
                do {
                    let blogPost = try JSONDecoder().decode(BlogPost.self, from: data)
                    blogPosts.append(blogPost)
                    dispatchGroup.leave()
                } catch {
                    dispatchGroup.leave()
                    completion(.failure(.failedToDecodeJSON))
                }
            }
            
            dataTask.resume()
        }
        
        dispatchGroup.notify(queue: .main) {
            completion(.success(blogPosts))
        }
    }
}
user10711707
  • 135
  • 1
  • 7
  • See for instance https://stackoverflow.com/a/50490094/341994 – matt Feb 27 '21 at 19:26
  • Point is, with you architecture you can’t stop any iteration’s network operation; it has already happened. So there is nothing to cancel. All you can do is ignore the responses when they arrive. – matt Feb 27 '21 at 19:34
  • You could have limited the number of simultaneous network operations, and then you could have skipped starting later operations after the failure. But you didn’t do that. – matt Feb 27 '21 at 19:40
  • 1
    Note that coherent API for solving this kind of problem is coming soon! https://github.com/DougGregor/swift-evolution/blob/structured-concurrency/proposals/0303-structured-concurrency.md – matt Mar 29 '21 at 20:37
  • Thanks for informing me about this! What I have done for now is put a local boolean variable and if at any point it fails, change the boolean value and then in dispatchGroup.notify(queue: .main) { ... } I check if all of them were successful or not. So that is how I avoid multiple failure completion callback. – user10711707 Mar 30 '21 at 15:27
  • But that does not do what you said: “However, if a single failure occur, I want to abort the entire operation”. – matt Mar 30 '21 at 17:21

0 Answers0