递归/循环NSURLSession异步完成处理程序

时间:2016-02-10 22:19:48

标签: swift

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

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

我发现这里有很多类似的帖子,很多都使用Dispatch Groups和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()
  }

1 个答案:

答案 0 :(得分:3)

首先,看起来很奇怪,没有带有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?) -> () )

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

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-like”期货的可行实施:

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