1
\$\begingroup\$

This is generic logic for handling network requests that load data based on pages. It is assumed that the view controller would implement the UISource protocol. I have a test suite as well. I'm especially interested in any ideas for doing away with the Variable. I'm also interested in ideas for refactoring this code into smaller functions.

import RxSwift


protocol UISource {
    var refreshTrigger: Observable<Void> { get } // reloads most recently loaded page
    var nextPageTrigger: Observable<Void> { get } // loads next page
    var bag: DisposeBag { get }
}

protocol NetworkSource {
    associatedtype Item
    func loadData(page: Int) -> Observable<[Item]>
}

enum Result<T> {
    case success(T)
    case failure(Error)
}

struct PaginationNetworkSink<T> {

    let loading: Observable<Bool> // true if network loading is in progress.
    let elements: Observable<[T]> // elements from all loaded pages
    let error: Observable<Error> // fires once for each error
}

func paginationNetworkLogic<T, API>(ui: UISource, api: API) -> PaginationNetworkSink<T> where API: NetworkSource, T == API.Item {
    let loadResults = Variable<[Int: [T]]>([:])

    let refreshOffset = ui.refreshTrigger
        .map { loadResults.value.keys.max() }
        .filter { $0 != nil }
        .map { $0! }

    let nextOffset = ui.nextPageTrigger.map { () -> Int in
        if let page = loadResults.value.keys.max() {
            return page + 1
        } else {
            return 0
        }
    }

    let pageToLoad = Observable.of(refreshOffset, nextOffset)
        .merge()
        .startWith(0)

    let loadResult = pageToLoad.flatMapLatest {
        api.loadData(page: $0)
            .map { Result.success($0) }
            .catchError { Observable.just(Result.failure($0)) }
    }.share()

    let successResult = loadResult.map { (result) -> [T] in
        if case .success(let array) = result {
            return array
        } else {
            return []
        }
    }

    let pageAndResult = Observable.zip(pageToLoad, successResult) { (page: $0.0, array: $0.1) }
        .filter { !$0.array.isEmpty }

    pageAndResult.subscribe(onNext: {
        loadResults.value[$0.page] = $0.array
    }).disposed(by: ui.bag)

    let startLoading = pageToLoad
        .map { _ in true }

    let loadingComplete = loadResult.map { _ in false }

    let loading = Observable.of(startLoading, loadingComplete)
        .merge()
        .startWith(true)

    let elements: Observable<[T]> = loadResults.asObservable().map { results in
        var result: [T] = []
        let keys = results.keys.sorted()
        for key in keys {
            result += results[key]!
        }
        return result
    }.startWith([])

    let error = loadResult.map { result -> Error? in
        if case .failure(let error) = result {
            return error
        } else {
            return nil
        }
    }.filter { $0 != nil }.map { $0! }

    return PaginationNetworkSink(loading: loading, elements: elements, error: error)
}
\$\endgroup\$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.