我正在使用一个 Web API,它提供的结果达到给定的限制(请求的 pageSize
参数)。如果结果数超过此限制,则响应消息会预先填充一个 URL,可以向该 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]?
}
在使用完成处理程序解决“旧样式”问题时,我不得不编写一个处理程序,它使用响应的 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 错误,解码响应并调用完成 block 。
现在我想使用 Combine 框架并使用自动提供分页响应的 Publisher,而无需调用另一个 Publisher。
因此,我编写了一个 requestPublisher
函数,它接受 request
和(初始)url
并返回一个 URLSession.dataTaskPublisher
检查 HTTP 错误(使用 tryMap
),解码
响应。
现在,我必须确保只要最后一个发出的值具有有效的 next
URL 并且完成事件发生,发布者就会自动“更新”自己。
我发现有一个 Publisher.append
方法可以准确地做到这一点,但我目前遇到的问题是:我只想在特定条件下追加(=valid 下一步
).
下面的伪代码说明了这一点:
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。一个新的
moreRequest
发布者,它为urlPublisher
的每个值获取一个新请求,将所有next
URL 发送回urlPublisher
.
然后我尝试使用 append
将 moreRequest
发布者附加到 mainRequest
。
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)
但这仍然不起作用......我总是得到“主要”请求的结果。缺少所有跟进请求。
最佳答案
看来我自己找到了解决办法。
我的想法是,我有一个 urlPublisher
,它使用第一个 URL 进行初始化,然后执行该 URL,并且可以将 next
URL 提供给 urlPublisher
并通过这样做引起后续请求。
let url = endpoint(for: request) // initial URL
let urlPublisher = CurrentValueSubject<URL, Error>(url)
urlPublisher
.flatMap {
return dataProvider.requestPublisher(for: request, with: $0)
.handleEvents(receiveOutput: {
if let next = $0.next, let moreURL = URL(string: self.transformNextUrl(nextUrl: next)) {
urlPublisher.send(moreURL)
} else {
urlPublisher.send(completion: .finished)
}
})
}
.sink(receiveCompletion: { print($0) }, receiveValue: { print($0) })
.store(in: &cancellableSet)
所以最后,我使用了两个发布者的组合和 flatMap
而不是非功能性的 append
。可能这也是人们从一开始就瞄准的解决方案......
关于ios - 可以使用什么 Combine 运算符/方法来加载 "paged API"的所有页面?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/59767427/