2016-09-19 54 views
6

Bir iOS uygulaması için ilk RxSwift projemi başlatıyorum ve reaktif programlama hakkında bilgi edinin.Sayfalandırılmış API Çağrılar RxSwift ile

Şimdiye kadar, bu fikir oldukça basittir, kullanıcı arama çubuğu metniyle eşleşen filmleri arar, bu da sonuçlarla birlikte bir UITableView'ı dolduran bir istek başlatır. Çevrimiçi bulduğunuz öğreticiler ve örnekler kullanarak bu bit'i çok fazla sorun yaşamadan gerçekleştirmeyi başardım.

Zor olan bölüm, tablo görüntüsünün altını kaydırarak tetiklenen sonuçların bir sonraki sayfasını yüklemeye çalıştığımda gelir. gözlemlenebilir pagesHomeViewController benim tablo görünümü binded edilir ve arama çubuğu metni searchText binded edilir:

İşte
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) 
    } 
} 

Bugüne kadar ne var: Burada

şimdiye kadar kullanılan koddur.

Sahnelerin arkasındaki API çağrılarını gerçekleştirmek için Alamofire kullanıyorum ve TMDbAPI.Films(withTitle: query), Göz attıran bir liste döndürüyor.

İşte benim model yapısı 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] 
    } 
} 

Şimdi bir sonraki sayfa için bir istek tetikleyecek bir şekilde sayfalara ayrılmış listelerin gözlemlenebilir benim loadNextPage değişken kanca reaktif bir yol arıyorum olduğunu. Ve arama çubuğu metni 0.

için sayfalama sıfırlamak istiyorum değiştiğinde ben operatörleri scan ve concat kullanılması gerekli ama hala emin değilim olacağını düşünüyoruz ...

herhangi bir öneriniz ... Bu çok takdir ulaşmak için nasıl

İşte

cevap

2

RxSwift GitHub repo ben bunu başardı birlikte verilen örneklere dayanarak.

Temel olarak, PaginatedList öğelerini akışımı döndüren bir özyinelemeli işlev kullanıyorum, bir sonraki sayfa için loadNextPage tetikleyicisi ile kendini çağırıyor. İşte benim API yöneticisi kullanılan kod:

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

Ve sonra benim bakış modelinde, bu yapmam gereken gereken tek şey:

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

bunu yapısı gibi uygulayabilirsiniz:

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

Çıktı:

SomePageResult (içerik: "Arama (zebra için bazı içerik), sayfa 1")
SomePageResult (içerik: "Arama (helikopter için bazı içerik), sayfa 1")
SomePageResult (içerik: "Bazı içerik için arama sayfa 2" (helikopter))
SomePageResult (içerik: "arama (tek boynuzlu için bazı içerik), sayfa 1")
SomePageResult (içerik: "arama (karınca için bazı içerik), sayfa 1")
SomePageResult (içerik: "arama için bazı içerik (karınca) sayfa 2")
SomePageResult (içerik: "arama için bazı içerik (karınca) sayfa 3")
SomePageResult (içerik: "sayfa 4 aramada (karınca) için bazı içerik")