web-dev-qa-db-ja.com

RxSwiftを使用したページ付けされたAPI呼び出し

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)
    }
}
_

これまでのところ、次のようになっています。監視可能なpagesHomeViewControllerのテーブルビューにバインドされ、その検索バーのテキストは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にリセットされます。

演算子scanconcatの使用が必要になると思いますが、それでも方法がわかりません...

これを達成する方法についての提案は大歓迎です...

11
Broco

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)
}
11
Broco

構造化する方法は次のとおりです。

// 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ページの検索用コンテンツ(ゼブラ)")
SomePageResult(content: "1ページの検索用コンテンツ(ヘリコプター)")
SomePageResult(content: "2ページの検索用コンテンツ(ヘリコプター)")
SomePageResult(content: "1ページの検索用コンテンツ(Unicorn)")
SomePageResult(content: "1ページの検索(ant)用のコンテンツ")
SomePageResult(content: "2ページの検索(ant)用のコンテンツ")
SomePageResult(content: "3ページの検索(ant)用のコンテンツ")
SomePageResult(content: "4ページの検索(ant)用のコンテンツ")

11
solidcell