使用 RxSwift 的分页 API 调用
Paginated API Calls with RxSwift
我正在为 iOS 应用程序开始我的第一个 RxSwift 项目并学习反应式编程。
到目前为止,这个想法非常简单,用户搜索与搜索栏文本匹配的电影,这会触发一个请求,用结果填充 UITableView。使用在线找到的教程和示例,我成功地实现了这一点。
棘手的部分出现在我试图加载通过滚动 table 视图底部触发的下一页结果时。
这是目前使用的代码:
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
中的 table 视图,其搜索栏文本绑定到 searchText
。
我正在使用 Alamofire 在幕后执行 API 调用,以及 TMDbAPI.Films(withTitle: query)
returns 分页列表的 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
,但我仍然不确定如何...
任何关于如何实现这一目标的建议将不胜感激...
以下是构建它的方法:
// 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([=10=]) }
.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: "some content for search (zebra) on page 1")
SomePageResult(content: "some content for search (helicopter) on page 1")
SomePageResult(content: "some content for search (helicopter) on page 2")
SomePageResult(content: "some content for search (unicorn) on page 1")
SomePageResult(content: "some content for search (ant) on page 1")
SomePageResult(content: "some content for search (ant) on page 2")
SomePageResult(content: "some content for search (ant) on page 3")
SomePageResult(content: "some content for search (ant) on page 4")
根据 RxSwift GitHub repo 中提供的示例,我设法做到了。
基本上,我使用的是 returns 我的 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)
}
我正在为 iOS 应用程序开始我的第一个 RxSwift 项目并学习反应式编程。
到目前为止,这个想法非常简单,用户搜索与搜索栏文本匹配的电影,这会触发一个请求,用结果填充 UITableView。使用在线找到的教程和示例,我成功地实现了这一点。
棘手的部分出现在我试图加载通过滚动 table 视图底部触发的下一页结果时。
这是目前使用的代码:
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
中的 table 视图,其搜索栏文本绑定到 searchText
。
我正在使用 Alamofire 在幕后执行 API 调用,以及 TMDbAPI.Films(withTitle: query)
returns 分页列表的 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
,但我仍然不确定如何...
任何关于如何实现这一目标的建议将不胜感激...
以下是构建它的方法:
// 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([=10=]) }
.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: "some content for search (zebra) on page 1")
SomePageResult(content: "some content for search (helicopter) on page 1")
SomePageResult(content: "some content for search (helicopter) on page 2")
SomePageResult(content: "some content for search (unicorn) on page 1")
SomePageResult(content: "some content for search (ant) on page 1")
SomePageResult(content: "some content for search (ant) on page 2")
SomePageResult(content: "some content for search (ant) on page 3")
SomePageResult(content: "some content for search (ant) on page 4")
根据 RxSwift GitHub repo 中提供的示例,我设法做到了。
基本上,我使用的是 returns 我的 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)
}