Home > Software design >  Stop function execution until URLSession is completed for recursive api calls
Stop function execution until URLSession is completed for recursive api calls

Time:02-05

Each API call uses a URLSession shared data task that stalls while the program executes further.

func callAPI(portfolio: Portfolio, symbol: String, endpoint: String){
    print("API CALL")
    let decoder = JSONDecoder()
    
    if let apiURL = URL(string: endpoint){
        var request = URLRequest(url: apiURL)
        request.httpMethod = "GET"
        URLSession.shared.dataTask(with: request) { (data, response, error) in
            print("IN URL SESSION")
            do{
                let decodedData = try decoder.decode(PriceResponse.self, from: data!)
                portfolio.portfolioList[symbol] = decodedData.self
                
            }catch let err{
                print(err)
            }
        }.resume()
    }
}

The above method is called recursively as such

func setPortfolioTableList(portfolio: Portfolio) {
    for (endpointKey, endpointValue) in portfolio.endpointList{
        print("Calling API for currency \(endpointKey)")
        callAPI(portfolio: portfolio, symbol: endpointKey, endpoint: endpointValue)
        
    }
    print(\(portfolio.coins))
 /*other code*/
}

The issue is that the print statement "IN URL SESSION" appears after print((portfolio.coins)). I need the setPortfolioTableList() method to await for all api calls to be made in the for loop before continuing to other code in the function.

I attempted implementing DispatchQueue and async/await but to no avail. All tips welcome

CodePudding user response:

You could use a dispatch group:

func callAPI(portfolio: Portfolio, symbol: String, endpoint: String, completion: @escaping () -> Void){
    // ...
        URLSession.shared.dataTask(with: request) { (data, response, error) in
            // ...
            completion()
        }.resume()
    }
}

func setPortfolioTableList(portfolio: Portfolio) {
    let dispatchGroup = DispatchGroup()

    for (endpointKey, endpointValue) in portfolio.endpointList{
        dispatchGroup.enter()
        print("Calling API for currency \(endpointKey)")
        callAPI(portfolio: portfolio, symbol: endpointKey, endpoint: endpointValue) {
            dispatchGroup.leave()   
        }
    }

    dispatchGroup.notify(queue: .main) {
        print(\(portfolio.coins))
        /*other code*/ 
    }
}

CodePudding user response:

You do not want to “stop execution”. You want to adopt asynchronous patterns.

For example, the callAPI should not be updating portfolio. It should pass back the result in a completion handler closure, e.g.:

let decoder = JSONDecoder() // no need to repeatedly instantiate a decoder used by all requests; move out of the method

@discardableResult
func callAPI(
    endpoint: String,
    queue: DispatchQueue = .main,
    completion: @escaping (Result<PriceResponse, Error>) -> Void
) -> URLSessionTask? {
    guard let url = URL(string: endpoint) else {
        queue.async { completion(.failure(URLError(.badURL))) }
        return nil
    }

    let task = URLSession.shared.dataTask(with: url) { [self] data, response, error in
        guard let data = data, error == nil else {
            queue.async { completion(.failure(error ?? URLError(.badServerResponse))) }
            return
        }

        do {
            let priceResponse = try decoder.decode(PriceResponse.self, from: data)
            queue.async { completion(.success(priceResponse)) }
        } catch let parseError {
            queue.async { completion(.failure(parseError)) }
        }
    }
    task.resume()
    return task
}

Note the completion handler closure with a Result<PriceResponse,Error> parameter. That's how we pass the data back. And I am providing an optional parameter, queue, so you can designate which queue you want the completion handlers called.

(I also pass back the URLSessionTask, in case you want to add cancelation logic in the future, but that's not relevant here.)

Then setPortfolioTableList would use that Result to update the model in the completion handler of callAPI:

func setPortfolioTableList(portfolio: Portfolio, completion: (() -> Void) = nil) {
    let group = DispatchGroup()

    for (symbol, endpoint) in portfolio.endpointList {
        group.enter()
        callAPI(endpoint: endpoint) { result in
            defer { group.leave() }

            switch result {
            case .failure(let error):         print(error)
            case .success(let priceResponse): portfolio.portfolioList[symbol] = priceResponse
            }
        }
    }

    group.notify(queue: .main) {
        // do something at the end

        ...

        // when done, call this methods completion handler

        completion?()
    }
}

Note, I've given this method an optional completion handler, too, so its caller can know when all of the network requests are done, should you need this. And I use a DispatchGroup to keep track of whether all of these asynchronous network requests are done, yet.

Now, apparently, your Portfolio is a class (a reference type) and you're updating it incrementally as these individual network requests finish. You might not want that. E.g., if the UI says “last updated 10 minutes ago”, but half of the prices are updated just now, and half are not, that might not be desirable. You might consider to defer the updating of the model object until everything is done, e.g.:

func setPortfolioTableList(portfolio: Portfolio, completion: @escaping () -> Void) {
    let group = DispatchGroup()

    var prices: [String: PriceResponse] = [:]

    for (symbol, endpoint) in portfolio.endpointList {
        group.enter()
        callAPI(endpoint: endpoint) { result in
            defer { group.leave() }

            switch result {
            case .failure(let error):         print(error)
            case .success(let priceResponse): prices[symbol] = priceResponse
            }
        }
    }

    group.notify(queue: .main) {
        // do something at the end

        portfolio.portfolioList = prices

        // when done, call this methods completion handler

        completion()
    }
}

I inferred from your use of dataTask(with:completion:) that you are not yet using the Swift Concurrency system (aka async-await). If you were, the implementation is simplified quite a bit:

func callAPI(endpoint: String) async throws -> PriceResponse {
    guard let url = URL(string: endpoint) else {
        throw URLError(.badURL)
    }

    let (data, _) = try await URLSession.shared.data(from: url)
    return try decoder.decode(PriceResponse.self, from: data)
}

func setPortfolioTableList(portfolio: Portfolio) async throws -> [String: PriceResponse] {
    try await withThrowingTaskGroup(of: (String, PriceResponse).self) { [self] group in
        for (symbol, endpoint) in portfolio.endpointList {
            group.addTask { try await (symbol, callAPI(endpoint: endpoint)) }
        }

        return try await group.reduce(into: [:]) { $0[$1.0] = $1.1 }
    }
}

But that assumes you don't need to support older OS versions, have familiarity with the new concurrency system, etc. If you are interested, see WWDC 2021 videos Meet async/await in Swift, Use async/await with URLSession, etc.

But the new concurrency system is the best of both worlds. It “awaits” the results, but is still a truly asynchronous pattern.

  •  Tags:  
  • Related