2016-02-10 5 views
1

Для использования результатов поиска в API используется несколько запросов. Он разработан таким образом, потому что поиск может занять много времени (> 5 минут). Первоначальный ответ немедленно возвращается к метаданным о поиске, и эти метаданные используются в последующих запросах до тех пор, пока поиск не будет завершен. Я не контролирую API.Рекурсивные/циклические обработчики завершения асинхронной обработки NSURLSession

  • первый запрос является POST к https://api.com/sessions/search/
  • Ответ на этот запрос содержит куки и метаданные о поиске. Важными полями в этом ответе являются search_cookie (a String) и (a)
  • 2-й запрос - это POST до https://api.com/sessions/results/ с добавленным к URL-адресу search_cookie. например https://api.com/sessions/results/c601eeb7872b7+0
  • Ответ на 2 запроса будет содержать либо:
    • Результаты поиска, если запрос завершен (он же search_completed_pct == 100)
    • Метаданные о ходе поиска, search_completed_pct является прогресс поиск и будет находиться в диапазоне от 0 до 100.
  • Если поиск не завершен, я хочу, чтобы сделать запрос каждые 5 секунд, пока не будет завершена (иначе search_completed_pct == 100)

Я нашел здесь многочисленные сообщения, которые похожи, многие используют группы отправки и для циклов, но этот подход не работает для меня. Я пробовал цикл 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() 
    } 

ответ

2

Ну прежде всего, это кажется странным, что нет API с запросом GET, который просто возвращает результат - даже если это может занять Минуты эс. Но, как вы упомянули, вы не можете изменить 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 или с отменяемым таймером отправки (нам нужно позже отказаться от отмены).

Я не показываю, как реализовать createSearch и fetchSearch. Они могут быть легко реализованы с использованием сторонней сетевой библиотеки или могут быть легко реализованы на основе NSURLSession.

Вывод:

Что может стать немного громоздким, чтобы реализовать обработку ошибок и отмены, а также решения всех обработчиков завершения. Чтобы решить эту проблему в сжатой и элегантной манере, я предложил бы использовать вспомогательную библиотеку, которая реализует «Обещания» или «Фьючерсы», или попытаться решить ее с помощью 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() 
+0

Это отлично работало, спасибо! Теперь я буду рассматривать Promise как лучший способ реализовать отмену. – Eric

+0

@ Эрик Если вас интересуют _Scala-like_ Фьючерсы и обещания, как показано выше, посмотрите здесь: [Яркие фьючерсы] (https://github.com/Thomvis/BrightFutures) или, как альтернатива [FutureLib] (https : //github.com/couchdeveloper/FutureLib). FutureLib также содержит отдельную независимую библиотеку Cancellation. Эта функция аннулирования полезна для полного скрытия основной задачи, например. ваш клиентский код не нуждается в ссылке на объект «NSURLSessionTask», чтобы иметь возможность отменить запрос. Я автор FutureLib. – CouchDeveloper

Смежные вопросы