Last active
August 21, 2016 12:42
-
-
Save TheAdamBorek/cc47555787237ea18d3f0af0a575dcb3 to your computer and use it in GitHub Desktop.
Inject an generic struct
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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() | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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