Skip to content

Instantly share code, notes, and snippets.

@TheAdamBorek
Last active August 21, 2016 12:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save TheAdamBorek/cc47555787237ea18d3f0af0a575dcb3 to your computer and use it in GitHub Desktop.
Save TheAdamBorek/cc47555787237ea18d3f0af0a575dcb3 to your computer and use it in GitHub Desktop.
Inject an generic struct
//
// Created by Adam Borek on 17.06.2016.
//
import Foundation
import RxSwift
import RxCocoa
class PostsListViewModel {
typealias Index = Int
let isRefreshing: Variable<Bool> = Variable(false)
let willDisplayPostAt: PublishSubject<Index> = PublishSubject()
private let subscribeScheduler: ImmediateSchedulerType
private let book: RxBook<Post>
/**
Initialize viewModel
- parameter postsProvider: Provider for posts
- parameter subscribeScheduler: scheduler where post should be downloaded. By default it is global queue. For convenience, you can change it for MainScheduler inside Unit Tests
- returns: PostsListViewModel
*/
init(book: RxBook<Post>, subscribeScheduler: ImmediateSchedulerType = ConcurrentDispatchQueueScheduler(globalConcurrentQueueQOS: .UserInitiated)) {
self.subscribeScheduler = subscribeScheduler
self.book = book
}
}
extension PostsListViewModel {
// For now this class only works for "successful" way. It doesn't care about any errors which is wrong. I would like to create unit tests, inject the stub of RxBook into it, and handle all situations. What is hard for me is that RxBook for now is an generic struct. I don't know how should I inject stubed version of it. Of course, current implementation of RxBook could be a class, but such solution would solve my problem just temporarly. It will move the problem in a time, until using structs will be justifed.
/// Returns Observable of posts which downloads initial page just after subscription & on every pull to refresh. It will also sends new array everytime when new page will arrived from server side.
var posts: Observable<[PostViewModel]> {
return nowAndOnRefreshDrag
.flatMap {
return self.book.receiveItems(displayingIndexChanged: self.willDisplayPostAt)
.map { $0.map(createPostViewModel) }
.subscribeOn(self.subscribeScheduler)
}.doOnNext { _ in
self.isRefreshing.value = false
}.shareReplayLatestWhileConnected()
}
private var nowAndOnRefreshDrag: Observable<Void> {
return [
Observable.just(),
isRefreshing.asObservable().filter { $0 }.mapToVoid()
].concat()
}
}
//
// Created by Adam Borek on 19/08/16.
//
import Foundation
import RxSwift
/**
* It is an implementation for pagination logic. It downloads pages from generic PageProvider and merge with existing items.
*/
struct RxBook<T> {
var pageSize: Int
private let loadNewPagesClosure: (alreadySeenItems: [T], pageLimit: Int) -> Observable<[T]>
init<PageProvider where PageProvider: PaginatedListProvider, PageProvider.Item == T>(pageProvider: PageProvider, pageSize: Int = 30) {
loadNewPagesClosure = pageProvider.downloadList
self.pageSize = pageSize
}
/**
It starts to download an inital page of items. It will also propagates updated set of items, everytime when new page will arrive.
- parameter indexChanged: Observable for index of item which is currently displayed. Based on that index RxBook calculate if it should downlaod new page or not
- returns: Observable of set of items to display
*/
func receiveItems(displayingIndexChanged indexChanged: Observable<Int>) -> Observable<[T]> {
return downloadNewPage(seen:[], displayingIndex: indexChanged)
}
private func downloadNewPage(seen seenItems: [T], displayingIndex: Observable<Int>) -> Observable<[T]> {
return self.loadNewPagesClosure(alreadySeenItems: seenItems, pageLimit:self.pageSize)
.savePage(into: seenItems)
.flatMap { currentItems -> Observable<[T]> in
return self.downloadNextPageWhenPossible(currentItems, displayingIndex: displayingIndex)
}
}
private func downloadNextPageWhenPossible(currentItems: [T], displayingIndex: Observable<Int>) -> Observable<[T]> {
let downloadNextPageTrigger = createTriggerWhenDownloadNewPage(from: displayingIndex, currentItems: currentItems)
return [
Observable.just(currentItems),
Observable.never().takeUntil(downloadNextPageTrigger),
self.downloadNewPage(seen: currentItems, displayingIndex: displayingIndex)
].concat()
}
private func createTriggerWhenDownloadNewPage(from displayingIndex: Observable<Int>, currentItems: [T]) -> Observable<Void> {
return displayingIndex.previousAndCurrent().filter { previousIndex, currentIndex in
return self.shouldDownloadNewPage(currentItems, currentIndex, previousIndex)
}.mapToVoid()
}
private func shouldDownloadNewPage(currentItems: [T], _ currentIndex: Int, _ previousIndex: Int) -> Bool {
return isForwardScrolling(currentIndex, previousIndex) &&
currentIndex >= currentItems.count / 2
}
private func isForwardScrolling(currentIndex: Int, _ previousIndex: Int) -> Bool {
return currentIndex > previousIndex
}
}
private extension ObservableType where E: _ArrayType {
func savePage(into items: Self.E) -> Observable<Self.E> {
return scan(items, accumulator: { items, page in
return items + page
})
}
}
protocol PaginatedListProvider {
associatedtype Item
func downloadList(itemsAlreadySeen: [Item], pageLimit: Int) -> Observable<[Item]>
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment