Broken API calls using RxSwift

I am starting my first RxSwift project for an iOS application and learn about reactive programming.

While the idea is quite simple: the user searches for movies that match the text of the search bar, this causes a query that populates the UITableView with results. Using the tutorials and examples found on the Internet, I was able to implement this bit without any problems.

The hard part comes up when I try to load the next page of results caused by scrolling the bottom of the table view.

Here is the code used so far:

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

Here's what I still have: the observed pages bound to my table view in the HomeViewController , and the text of its search string is bound to searchText .

I use Alamofire to make API calls backstage, and TMDbAPI.Films(withTitle: query) returns the Observed number of broken lists.

Here is my PaginatedList model structure

 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] } } 

Now I'm looking for a reactive way to bind my variable loadNextPage to the observable from the broken lists so that it calls up the next page. And when the text of the search bar changes, pagination will reset to 0.

I believe that using scan and concat would be necessary, but I'm still not sure how ...

Any suggestions on how to achieve this would be greatly appreciated ...

+6
source share
2 answers

Based on the examples provided in the RxSwift GitHub repo , I managed to do this.

Basically, I use a recursive function that returns my PaginatedList element PaginatedList , it calls itself by running loadNextPage for the next page. Here is the code I used in my API manager:

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

And then, in my opinion, this is all I need to do:

 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) } 
+3
source

Here is how you could structure it:

 // 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() 

Output:

SomePageResult (content: "some searchable content (zebra) on page 1")
SomePageResult (content: "some content to search for (helicopter) on page 1")
SomePageResult (content: "some content to search for (helicopter) on page 2")
SomePageResult (content: "some searchable content (unicorn) on page 1")
SomePageResult (content: "some content to search for (ant) on page 1")
SomePageResult (content: "some content to search for (ant) on page 2")
SomePageResult (content: "some searchable content (ant) on page 3")
SomePageResult (content: "some content to search for (ant) on page 4")

+7
source

All Articles