使用 RxSwift 进行分页 API 调用
Posted
技术标签:
【中文标题】使用 RxSwift 进行分页 API 调用【英文标题】:Paginated API Calls with RxSwift 【发布时间】:2016-09-19 23:28:20 【问题描述】:我正在开始我的第一个 ios 应用程序的 RxSwift 项目并学习反应式编程。
到目前为止,这个想法很简单,用户搜索与搜索栏文本匹配的电影,这会触发一个使用结果填充 UITableView 的请求。使用在线找到的教程和示例,我已经成功地实现了这一点,没有太多麻烦。
当我尝试加载由滚动表格视图底部触发的下一页结果时,棘手的部分就出现了。
这是目前使用的代码:
public final class HomeViewModel: NSObject
// MARK: - Properties
var searchText: Variable<String> = Variable("")
var loadNextPage: Variable<Void> = Variable()
lazy var pages: Observable<PaginatedList<Film>> = self.setupPages()
// MARK: - Reactive Setup
fileprivate func setupPages() -> Observable<PaginatedList<Film>>
return self.searchText
.asObservable()
.debounce(0.3, scheduler: MainScheduler.instance)
.distinctUntilChanged()
.flatMapLatest (query) -> Observable<PaginatedList<Film>> in
return TMDbAPI.Films(withTitle: query, atPage: 0)
.shareReplay(1)
这是我目前所拥有的:可观察的pages
绑定到HomeViewController
中的我的表格视图,并且它的搜索栏文本绑定到searchText
。
我正在使用 Alamofire 在后台执行 API 调用,TMDbAPI.Films(withTitle: query)
返回一个分页列表的 Observable。
这是我的模型结构PaginatedList
public struct PaginatedList<T>
// MARK: - Properties
let page: Int
let totalResults: Int
let totalPages: Int
let results: [T]
// MARK: - Initializer
init(page: Int, totalResults: Int, totalPages: Int, results: [T])
self.page = page
self.totalResults = totalResults
self.totalPages = totalPages
self.results = results
// MARK: - Helper functions / properties
var count: Int return self.results.count
var nextPage: Int?
let nextPage = self.page + 1
guard nextPage < self.totalPages else return nil
return nextPage
static func Empty() -> PaginatedList return PaginatedList(page: 0, totalResults: 0, totalPages: 0, results: [])
extension PaginatedList
// MARK: - Subscript
subscript(index: Int) -> T
return self.results[index]
我现在正在寻找一种反应方式来将我的loadNextPage
变量挂钩到可观察的分页列表,从而触发对下一页的请求。
当搜索栏文本发生变化时,它会将分页重置为 0。
我相信需要使用运算符 scan
和 concat
,但我仍然不确定如何...
任何关于如何实现这一点的建议将不胜感激......
【问题讨论】:
【参考方案1】:根据RxSwift GitHub repo 中提供的示例,我设法做到了。
基本上,我使用了一个递归函数,它返回我的PaginatedList
项目流,它使用下一页的loadNextPage 触发器调用自身。
这是我在 API 管理器中使用的代码:
class func films(withTitle title: String, startingAtPage page: Int = 0, loadNextPageTrigger trigger: Observable<Void> = Observable.empty()) -> Observable<[Film]>
let parameters: FilmSearchParameters = FilmSearchParameters(query: title, atPage: page)
return TMDbAPI.instance.films(fromList: [], with: parameters, loadNextPageTrigger: trigger)
fileprivate func films(fromList currentList: [Film], with parameters: FilmSearchParameters, loadNextPageTrigger trigger: Observable<Void>) -> Observable<[Film]>
return self.films(with: parameters).flatMap (paginatedList) -> Observable<[Film]> in
let newList = currentList + paginatedList.results
if let _ = paginatedList.nextPage
return [
Observable.just(newList),
Observable.never().takeUntil(trigger),
self.films(fromList: newList, with: parameters.nextPage, loadNextPageTrigger: trigger)
].concat()
else return Observable.just(newList)
fileprivate func films(with parameters: FilmSearchParameters) -> Observable<PaginatedList<Film>>
guard !parameters.query.isEmpty else return Observable.just(PaginatedList.Empty())
return Observable<PaginatedList<Film>>.create (observer) -> Disposable in
let request = Alamofire
.request(Router.searchFilms(parameters: parameters))
.validate()
.responsePaginatedFilms(queue: nil, completionHandler: (response) in
switch response.result
case .success(let paginatedList):
observer.onNext(paginatedList)
observer.onCompleted()
case .failure(let error):
observer.onError(error)
)
return Disposables.create request.cancel()
然后在我的视图模型中,这就是我所要做的:
fileprivate func setupFilms() -> Observable<[Film]>
let trigger = self.nextPageTrigger.asObservable().debounce(0.2, scheduler: MainScheduler.instance)
return self.textSearchTrigger
.asObservable()
.debounce(0.3, scheduler: MainScheduler.instance)
.distinctUntilChanged()
.flatMapLatest (query) -> Observable<[Film]> in
return TMDbAPI.films(withTitle: query, loadNextPageTrigger: trigger)
.shareReplay(1)
【讨论】:
【参考方案2】:你可以这样组织它:
// Some kind of page request result. Modify it to be what you're using.
struct SomePageResult
let content: String
// Needs modification to return your actual data
func getPage(query: String, number: UInt) -> SomePageResult
return SomePageResult(content: "some content for search (\(query)) on page \(number)")
// Actual implementation
let disposeBag = DisposeBag()
var loadNextPage = PublishSubject<Void>()
var searchText = PublishSubject<String>()
let currentPage = searchText
.distinctUntilChanged()
.flatMapLatest searchText in
return loadNextPage.asObservable()
.startWith(())
.scan(0) (pageNumber, _) -> UInt in
pageNumber + 1
.map pageNumber in
(searchText, pageNumber)
.map (searchText, pageNumber) in
getPage(searchText, number: pageNumber)
currentPage
.subscribeNext print($0)
.addDisposableTo(disposeBag)
searchText.onNext("zebra")
searchText.onNext("helicopter")
loadNextPage.onNext()
searchText.onNext("unicorn")
searchText.onNext("unicorn")
searchText.onNext("ant")
loadNextPage.onNext()
loadNextPage.onNext()
loadNextPage.onNext()
输出:
SomePageResult(content: "第 1 页上的一些搜索内容 (zebra)") SomePageResult(content: "第 1 页搜索(直升机)的一些内容") SomePageResult(content: "第 2 页搜索(直升机)的一些内容") SomePageResult(content: "第 1 页上的一些搜索内容(独角兽)") SomePageResult(content: "第 1 页上用于搜索 (ant) 的一些内容") SomePageResult(content: "第 2 页的一些搜索 (ant) 内容") SomePageResult(content: "第 3 页上的一些搜索 (ant) 内容") SomePageResult(content: "第4页搜索(蚂蚁)的一些内容")
【讨论】:
以上是关于使用 RxSwift 进行分页 API 调用的主要内容,如果未能解决你的问题,请参考以下文章
如何使用 RxSwift 和 alamofire 获得嵌套 api 调用的响应?