【问题标题】:Stop function execution until URLSession is completed for recursive api calls停止函数执行,直到 URLSession 完成递归 api 调用
【发布时间】:2022-02-05 21:49:10
【问题描述】:

每个 API 调用都使用一个 URLSession 共享数据任务,该任务会在程序进一步执行时停止。

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()
    }
}

上面的方法就是这样递归调用的

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*/
}

问题是打印语句“IN URL SESSION”出现在 print((portfolio.coins)) 之后。在继续执行函数中的其他代码之前,我需要 setPortfolioTableList() 方法来等待在 for 循环中进行的所有 api 调用。

我尝试实现 DispatchQueue 和 async/await 但无济于事。欢迎所有提示

【问题讨论】:

    标签: swift async-await grand-central-dispatch urlsession


    【解决方案1】:

    您可以使用调度组:

    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*/ 
        }
    }
    

    【讨论】:

      【解决方案2】:

      您不想“停止执行”。您想采用异步模式。

      传统模式是“完成处理程序”闭包参数。例如,与其让callAPI 直接更新portfolio,不如直接在完成处理程序闭包中传回结果,例如:

      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
      }
      

      注意带有Result&lt;PriceResponse,Error&gt; 参数的完成处理程序闭包。这就是我们将数据传回的方式。我提供了一个可选参数queue,因此您可以指定要调用完成处理程序的队列。

      (我也传回URLSessionTask,以防您将来想添加取消逻辑,但这与此处无关。)

      然后setPortfolioTableList 将使用该Result 来更新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?()
          }
      }
      

      注意,我也为这个方法提供了一个可选的完成处理程序,因此如果您需要,它的调用者可以知道所有网络请求何时完成。我使用DispatchGroup 来跟踪是否所有这些异步网络请求都已完成。

      现在,显然,您的 Portfolioclass(引用类型),并且随着这些单独的网络请求完成,您将逐步更新它。你可能不想要那个。例如,如果 UI 显示“上次更新时间是 10 分钟前”,但一半的价格刚刚更新,一半没有更新,这可能是不可取的。您可能会考虑推迟模型对象的更新,直到一切都完成,例如:

      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()
          }
      }
      

      我从您对dataTask(with:completion:) 的使用推断出您还没有使用 Swift 并发系统(又名async-await)。如果是的话,实现会大大简化:

      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 }
          }
      }
      

      但这假设您不需要支持旧操作系统版本,不熟悉新的并发系统等。如果您有兴趣,请观看 WWDC 2021 视频Meet async/await in SwiftUse async/await with URLSession 等。

      但是新的并发系统是两全其美的。它“等待”结果,但仍然是真正的异步模式。

      【讨论】:

      • 哈哈我想我应该在打字之前等待你的回答。 @San X 你绝对应该接受这个答案而不是我的,它更精致,也显示了更现代的 async/await 解决方案
      猜你喜欢
      • 2021-12-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-11-11
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多