Я работаю с веб-API, который доставляет результаты до заданного предела (pageSize
параметр запроса). Если число результатов превышает этот предел, ответное сообщение предварительно заполняется URL-адресом, по которому можно выполнить дополнительный запрос для получения большего количества результатов. Если есть еще больше результатов, это снова указывается тем же способом.
Мое намерение - получить все результаты сразу.
В настоящее время у меня есть что-то вроде следующих структур запросов и ответов:
// Request structure
struct TvShowsSearchRequest {
let q: String
let pageSize: Int?
}
// Response structure
struct TvShowsSearchResponse: Decodable {
let next: String?
let total : Int
let searchTerm : String
let searchResultListShow: [SearchResult]?
}
При решении проблемы ' old style ' с использованием обработчиков завершения мне пришлось написать обработчик, который вызывает запрос 'handle more' с URL-адресом ответа:
func handleResponse(request: TvShowsSearchRequest, result: Result<TvShowsSearchResponse, Error>) -> Void {
switch result {
case .failure(let error):
fatalError(error.localizedDescription)
case .success(let value):
print("> Total number of shows matching the query: \(value.total)")
print("> Number of shows fetched: \(value.searchResultListShow?.count ?? 0)")
if let moreUrl = value.next {
print("> URL to fetch more entries \(moreUrl)")
// start recursion here: a new request, calling the same completion handler...
dataProvider.handleMore(request, nextUrl: moreUrl, completion: handleResponse)
}
}
}
let request = TvShowsSearchRequest(query: "A", pageSize: 50)
dataProvider.send(request, completion: handleResponse)
Внутренне функции send
и handleMore
вызывают один и тот же internalSend
, который принимает request
и url
, для последующего вызова URLSession.dataTask(...)
, проверки на наличие ошибок HTTP, декодирования ответа и вызова блока завершения.
Теперь я хочу использовать платформу Combine и использовать Publisher, который автоматически предоставляет постраничные ответы, без необходимости вызова другого Publisher.
Поэтому я написал requestPublisher
функцию, которая принимает request
и (начальный) url
и возвращает URLSession.dataTaskPublisher
, который проверяет HTTP-ошибки (используя tryMap
), decode
ответ.
Теперь я должен убедиться, что Publisher автоматически " обновляет себя всякий раз, когда у последнего переданного значения был действительный URL-адрес next
, и происходит событие завершения.
Я обнаружил, что существует метод Publisher.append
, который точно сделает это, но у меня возникла проблема пока: я хочу добавить только при определенном условии (= valid next
).
Следующий псевдокод иллюстрирует это:
func requestPublisher(for request: TvShowsSearchRequest, with url: URL) -> AnyPublisher<TvShowsSearchResponse, Error> {
// ... build urlRequest, skipped here ...
let apiCall = self.session.dataTaskPublisher(for: urlRequest)
.tryMap { data, response -> Data in
guard let httpResponse = response as? HTTPURLResponse else {
throw APIError.server(message: "No HTTP response received")
}
if !(200...299).contains(httpResponse.statusCode) {
throw APIError.server(message: "Server respondend with status: \(httpResponse.statusCode)")
}
return data
}
.decode(type: TvShowsSearchResponse.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
return apiCall
}
// Here I'm trying to use the Combine approach
var moreURL : String?
dataProvider.requestPublisher(request)
.handleEvents(receiveOutput: {
moreURL = $0.next // remember the "next" to fetch more data
})
.append(dataProvider.requestPublisher(request, next: moreURL)) // this does not work, because moreUrl was not prepared at the time of creation!!
.sink(receiveCompletion: { print($0) }, receiveValue: { print($0) })
.store(in: &cancellableSet)
Я предполагаю, что есть люди там которые уже решили эту проблему реактивным путем. Всякий раз, когда я нахожу выполнимое решение, оно снова включает рекурсию. Я не думаю, что именно так должно выглядеть правильное решение.
Я ищу издателя, который отправляет ответы, а я не предоставляю функцию обратного вызова. Вероятно, должно быть решение с использованием Publisher of Publishers, но я еще не понимаю его.
После комментария @kishanvekariya я попытался собрать все с несколькими издателями:
Издатель mainRequest
, который получает ответ на "основной" запрос.
Новый urlPublisher
, который получает все next
URL-адреса "основного" или любых последующих запросов.
A новый moreRequest
издатель, который извлекает для каждого значения urlPublisher
новый запрос, отправляя все next
URL-адреса обратно на urlPublisher
.
Затем я попытался прикрепить moreRequest
издатель на mainRequest
с append
.
var urlPublisher = PassthroughSubject<String, Error>()
var moreRequest = urlPublisher
.flatMap {
return dataProvider.requestPublisher(request, next: $0)
.handleEvents(receiveOutput: {
if let moreURL = $0.next {
urlPublisher.send(moreURL)
}
})
}
var mainRequest = dataProvider.requestPublisher(request)
.handleEvents(receiveOutput: {
if let moreURL = $0.next {
urlPublisher.send(moreURL)
}
})
.append(moreRequest)
.sink(receiveCompletion: { print($0) }, receiveValue: { print($0) })
.store(in: &cancellableSet)
Но это все равно не работает ... Я всегда получаю результат "основного" запроса. Все последующие запросы отсутствуют.