ios - 可以使用什么 Combine 运算符/方法来加载 "paged API"的所有页面?

标签 ios swift combine

我正在使用一个 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)

在内部,sendhandleMore 函数都调用相同的 internalSend,它接受 requesturl,之后调用 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 发表评论后,我尝试与多个发布者一起构建所有内容:

  1. 正在获取对“主要”请求的响应的 mainRequest 发布者。

  2. 一个新的 urlPublisher,它正在接收“主要”或任何后续请求的所有 next URL。

  3. 一个新的 moreRequest 发布者,它为 urlPublisher 的每个值获取一个新请求,将所有 next URL 发送回urlPublisher.

然后我尝试使用 appendmoreRequest 发布者附加到 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/

相关文章:

ios - 引用架构 x86_64 : "_sqlite3_close", 的 undefined symbol

ios - 附件披露指示器未显示在自定义单元格中

objective-c - 类扩展中@property(非原子,只读)和@property 之间的区别?

swift - 对一系列协议(protocol)进行排序,寻找父协议(protocol)

swift - 有没有办法在 SwiftUI View 和与其间接相关的类之间设置一个全局变量?

异步下载图像时,SwiftUI 和 Combine 工作不流畅

ios - dataTaskPublisher失败后如何重启?

ios - 按收到日期的顺序显示两个自定义对象数组

ios - 多个命令产生错误 - Cocoapods 多个目标和平台

ios - 如何使用 dataTaskPublisher 链接多个 API 调用?