【问题标题】:Recursive/looping NSURLSession async completion handlers递归/循环 NSURLSession 异步完成处理程序
【发布时间】:2016-04-29 02:35:48
【问题描述】:

我使用的 API 需要多次请求才能获得搜索结果。之所以这样设计,是因为搜索可能需要很长时间(> 5 分钟)。初始响应立即返回有关搜索的元数据,并且该元数据用于后续请求,直到搜索完成。我不控制 API。

  • 第一个请求是对https://api.com/sessions/search/ 的 POST
  • 对此请求的响应包含关于搜索的 cookie 和元数据。此响应中的重要字段是 search_cookie(字符串)和 search_completed_pct(整数)
  • 第二个请求是对 https://api.com/sessions/results/ 的 POST,并在 URL 后附加了 search_cookie。例如https://api.com/sessions/results/c601eeb7872b7+0
  • 对第二个请求的响应将包含:
    • 查询完成后的搜索结果(又名search_completed_pct == 100)
    • 关于搜索进度的元数据,search_completed_pct 是搜索进度,介于 0 到 100 之间。
  • 如果搜索未完成,我想每 5 秒发出一次请求,直到完成(又名 search_completed_pct == 100)

我在这里发现了许多类似的帖子,其中很多都使用调度组和 for 循环,但这种方法对我不起作用。我尝试了一个while循环,但变量范围存在问题。调度组也不适合我。这闻起来像是走错路了,但我不确定。

我正在寻找合适的设计来进行这些递归调用。我应该使用代表还是闭包+循环要走的路?我碰壁了,需要一些帮助。

下面的代码是我尝试过的大致思路(为清楚起见进行了编辑。没有 dispatch_groups()、错误处理、json 解析等)

Viewcontroller.swift

apiObj.sessionSearch(domain) { result in
  Log.info!.message("result: \(result)")
})

ApiObj.swift

func sessionSearch(domain: String, sessionCompletion: (result: SearchResult) -> ()) {

      // Make request to /search/ url
      let task = session.dataTaskWithRequest(request) { data, response, error in 
        let searchCookie = parseCookieFromResponse(data!)

       *********  pseudo code  **************
        var progress: Int = 0
        var results = SearchResults()

        while (progress != 100) {

          // Make requests to /results/ until search is complete  
          self.getResults(searchCookie) { searchResults in 
                progress = searchResults.search_pct_complete
            if (searchResults == 100) {
             completion(searchResults)
            } else {
              sleep(5 seconds)
            } //if
          } //self.getResults()
        } //while
     *********  pseudo code  ************
    } //session.dataTaskWithRequest(
 task.resume()
 }


func getResults(cookie: String, completion: (searchResults: NSDictionary) -> ()) 

      let request = buildRequest((domain), url: NSURL(string: ResultsUrl)!)
      let session = NSURLSession.sharedSession()
      let task = session.dataTaskWithRequest(request) { data, response, error in 
        let theResults = getJSONFromData(data!)
        completion(theResults)
     }
  task.resume()
  }

【问题讨论】:

    标签: swift


    【解决方案1】:

    首先,似乎很奇怪,没有带有 GET 请求的 API 可以简单地返回结果 - 即使这可能需要几分钟。但是,正如您所提到的,您无法更改 API。

    因此,根据您的描述,我们需要发出一个有效“轮询”服务器的请求。我们这样做直到我们检索到一个 已完成Search 对象。

    因此,一种可行的方法将特意定义以下函数和类:

    从服务器返回的“搜索”对象的协议:

    public protocol SearchType {
        var searchID: String { get }
        var isCompleted: Bool { get }
        var progress: Double { get }
        var result: AnyObject? { get }
    }
    

    在客户端使用具体的结构或类。

    向服务器发出请求以创建搜索对象(您的#1 POST 请求)的异步函数:

    func createSearch(completion: (SearchType?, ErrorType?) -> () )
    

    然后是另一个异步函数,它获取一个“搜索”对象,如果它完成了可能会得到结果:

    func fetchSearch(searchID: String, completion: (SearchType?, ErrorType?) -> () )
    

    现在,一个异步函数获取某个“searchID”(您的“search_cookie”)的结果 - 并在内部实现轮询:

    func fetchResult(searchID: String, completion: (AnyObject?, ErrorType?) -> () )
    

    fetchResult 的实现现在可能如下所示:

    func fetchResult(searchID: String, 
        completion: (AnyObject?, ErrorType?) -> () ) {
        func poll() {
            fetchSearch(searchID) { (search, error) in
                if let search = search {
                    if search.isCompleted {
                        completion(search.result!, nil)
                    } else {
                        delay(1.0, f: poll)
                    }
                } else {
                    completion(nil, error)
                }
            }
        }
        poll()
    }
    

    此方法使用本地函数poll 来实现轮询功能。 poll 调用 fetchSearch 并在完成时检查搜索是否完成。如果不是,它会延迟一段时间,然后再次调用poll。这看起来像一个递归调用,但实际上它不是因为poll 在再次调用时已经完成。本地函数似乎适合这种方法。

    函数delay 只是等待指定的秒数,然后调用提供的闭包。 delay 可以通过 dispatch_after 或带有可取消调度计时器的方式轻松实现(我们需要稍后实现取消)。

    我没有展示如何实现createSearchfetchSearch。这些可以使用第三方网络库轻松实现,也可以基于NSURLSession轻松实现。

    结论:

    可能会变得有点麻烦的是实现错误处理和取消,以及处理所有的完成处理程序。为了以简洁优雅的方式解决这个问题,我建议使用实现“Promises”或“Futures”的辅助库 - 或尝试使用 Rx 解决它。

    例如一个使用“类 Scala”期货的可行实现:

    func fetchResult(searchID: String) -> Future<AnyObject> {
        let promise = Promise<AnyObject>()
        func poll() {
            fetchSearch(searchID).map { search in
                if search.isCompleted {
                    promise.fulfill(search.result!)
                } else {
                    delay(1.0, f: poll)
                }
            }
        }
        poll()
        return promise.future!
    }
    

    你会开始得到如下所示的结果:

    createSearch().flatMap { search in
        fetchResult(search.searchID).map { result in
            print(result)
        }
    }.onFailure { error in
        print("Error: \(error)")
    }
    

    以上包含完整的错误处理。它尚不包含取消。您确实需要实现一种取消请求的方法,否则可能无法停止轮询。

    使用“CancellationToken”实现取消的解决方案可能如下所示:

    func fetchResult(searchID: String, 
        cancellationToken ct: CancellationToken) -> Future<AnyObject> {
        let promise = Promise<AnyObject>()
        func poll() {
            fetchSearch(searchID, cancellationToken: ct).map { search in
                if search.isCompleted {
                    promise.fulfill(search.result!)
                } else {
                    delay(1.0, cancellationToken: ct) { ct in
                        if ct.isCancelled {
                            promise.reject(CancellationError.Cancelled)
                        } else {
                            poll()
                        }
                    }
                }
            }
        }
        poll()
        return promise.future!
    }
    

    它可能被称为:

    let cr = CancellationRequest()
    let ct = cr.token
    createSearch(cancellationToken: ct).flatMap { search in
        fetchResult(search.searchID, cancellationToken: ct).map { result in
            // if we reach here, we got a result
            print(result)
        }
    }.onFailure { error in
        print("Error: \(error)")
    }
    

    稍后您可以取消请求,如下所示:

    cr.cancel()
    

    【讨论】:

    • 效果很好,谢谢!我现在将研究 Promises 作为实现取消的更好方法。
    • @Eric 如果你对上面显示的 Scala-like Futures 和 Promise 感兴趣,请看这里:Bright FuturesFutureLib。 FutureLib 还包含一个独立的 Cancellation 库。此取消功能对于完全隐藏底层任务很有用,例如您的客户端代码不需要引用 NSURLSessionTask 对象即可取消请求。我是 FutureLib 的作者。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-25
    • 2019-01-13
    • 2018-09-01
    • 2015-12-21
    • 1970-01-01
    相关资源
    最近更新 更多