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