initial PaginationTableViewWrapper. api breaking changes in CursorType protocol

This commit is contained in:
Ivan Smolin 2017-04-04 16:25:00 +03:00
parent f9fbfb2735
commit ded59ff2d9
11 changed files with 99 additions and 58 deletions

View File

@ -12,11 +12,12 @@
674743941E929A5A00B47671 /* PaginationViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 674743931E929A5A00B47671 /* PaginationViewModelTests.swift */; };
675D24B21E9234BB00E92D1F /* PaginationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 675D24B11E9234BB00E92D1F /* PaginationViewModel.swift */; };
67788F9F1E69661800484DEE /* CGFloat+Pixels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67788F9E1E69661800484DEE /* CGFloat+Pixels.swift */; };
678A202A1E93C1A900787562 /* PaginationTableViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 678A20291E93C1A900787562 /* PaginationTableViewWrapper.swift */; };
67B3057B1E8A8727008169CA /* TestView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 67B3057A1E8A8727008169CA /* TestView.xib */; };
67B3057D1E8A8735008169CA /* TestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67B3057C1E8A8735008169CA /* TestView.swift */; };
67B3057F1E8A8804008169CA /* LoadFromNibTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67B3057E1E8A8804008169CA /* LoadFromNibTests.swift */; };
67B305841E8A92E8008169CA /* XibView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67B305831E8A92E8008169CA /* XibView.swift */; };
67B856E31E923BE600F54304 /* ResettableCursorType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67B856E21E923BE600F54304 /* ResettableCursorType.swift */; };
67B856E31E923BE600F54304 /* ResettableType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67B856E21E923BE600F54304 /* ResettableType.swift */; };
67EF144C1E8BEACB00D6E0DD /* StubCursor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67EF144B1E8BEACB00D6E0DD /* StubCursor.swift */; };
67EF144E1E8BED4E00D6E0DD /* CursorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67EF144D1E8BED4E00D6E0DD /* CursorTests.swift */; };
78011A641D47ABC500EA16A2 /* UIView+DefaultReuseIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78011A631D47ABC500EA16A2 /* UIView+DefaultReuseIdentifier.swift */; };
@ -105,11 +106,12 @@
674743931E929A5A00B47671 /* PaginationViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaginationViewModelTests.swift; sourceTree = "<group>"; };
675D24B11E9234BB00E92D1F /* PaginationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaginationViewModel.swift; sourceTree = "<group>"; };
67788F9E1E69661800484DEE /* CGFloat+Pixels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CGFloat+Pixels.swift"; sourceTree = "<group>"; };
678A20291E93C1A900787562 /* PaginationTableViewWrapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaginationTableViewWrapper.swift; sourceTree = "<group>"; };
67B3057A1E8A8727008169CA /* TestView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = TestView.xib; sourceTree = "<group>"; };
67B3057C1E8A8735008169CA /* TestView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestView.swift; sourceTree = "<group>"; };
67B3057E1E8A8804008169CA /* LoadFromNibTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadFromNibTests.swift; sourceTree = "<group>"; };
67B305831E8A92E8008169CA /* XibView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XibView.swift; sourceTree = "<group>"; };
67B856E21E923BE600F54304 /* ResettableCursorType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResettableCursorType.swift; sourceTree = "<group>"; };
67B856E21E923BE600F54304 /* ResettableType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResettableType.swift; sourceTree = "<group>"; };
67EF144B1E8BEACB00D6E0DD /* StubCursor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StubCursor.swift; sourceTree = "<group>"; };
67EF144D1E8BED4E00D6E0DD /* CursorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CursorTests.swift; sourceTree = "<group>"; };
78011A631D47ABC500EA16A2 /* UIView+DefaultReuseIdentifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+DefaultReuseIdentifier.swift"; sourceTree = "<group>"; };
@ -220,6 +222,7 @@
isa = PBXGroup;
children = (
675D24B11E9234BB00E92D1F /* PaginationViewModel.swift */,
678A20291E93C1A900787562 /* PaginationTableViewWrapper.swift */,
);
path = Pagination;
sourceTree = "<group>";
@ -533,7 +536,7 @@
783423691DB8D0E100A79643 /* StoryboardProtocol.swift */,
78CFEE4F1C5C45E500F50370 /* ViewHeightProtocol.swift */,
78CFEE501C5C45E500F50370 /* ViewModelProtocol.swift */,
67B856E21E923BE600F54304 /* ResettableCursorType.swift */,
67B856E21E923BE600F54304 /* ResettableType.swift */,
);
path = Protocols;
sourceTree = "<group>";
@ -886,6 +889,7 @@
7873D1511E112B0D001816EB /* Any+Cast.swift in Sources */,
78B036431DA4FEC90021D5CC /* CGImage+Transform.swift in Sources */,
78011A641D47ABC500EA16A2 /* UIView+DefaultReuseIdentifier.swift in Sources */,
678A202A1E93C1A900787562 /* PaginationTableViewWrapper.swift in Sources */,
786D78EC1D53C46E006B2CEA /* AlamofireManager+Extensions.swift in Sources */,
CAA707D51E2E614E0022D732 /* ModuleConfigurator.swift in Sources */,
675D24B21E9234BB00E92D1F /* PaginationViewModel.swift in Sources */,
@ -925,7 +929,7 @@
CAA707D71E2E616D0022D732 /* BaseViewModel.swift in Sources */,
E126CBB31DB68DDA00E1B2F8 /* UICollectionView+CellRegistration.swift in Sources */,
78CFEE581C5C45E500F50370 /* StaticViewHeightProtocol.swift in Sources */,
67B856E31E923BE600F54304 /* ResettableCursorType.swift in Sources */,
67B856E31E923BE600F54304 /* ResettableType.swift in Sources */,
787783631CA03CA0001CDC9B /* IndexPath+ImmutableIndexPath.swift in Sources */,
78B036471DA5624D0021D5CC /* CGImage+Creation.swift in Sources */,
789CC6081DE5835600F789D3 /* CursorType.swift in Sources */,

View File

@ -23,9 +23,7 @@
import RxSwift
/// Paging cursor implementation with enclosed cursor for fetching results
public class FixedPageCursor<Cursor: CursorType>: CursorType where Cursor.LoadResultType == CountableRange<Int> {
public typealias LoadResultType = CountableRange<Int>
public class FixedPageCursor<Cursor: CursorType>: CursorType {
private let cursor: Cursor
@ -53,11 +51,11 @@ public class FixedPageCursor<Cursor: CursorType>: CursorType where Cursor.LoadRe
return cursor[index]
}
public func loadNextBatch() -> Observable<LoadResultType> {
public func loadNextBatch() -> Observable<[Cursor.Element]> {
return loadNextBatch(withSemaphore: semaphore)
}
private func loadNextBatch(withSemaphore semaphore: DispatchSemaphore?) -> Observable<LoadResultType> {
private func loadNextBatch(withSemaphore semaphore: DispatchSemaphore?) -> Observable<[Cursor.Element]> {
return Observable.deferred {
semaphore?.wait()
@ -71,7 +69,7 @@ public class FixedPageCursor<Cursor: CursorType>: CursorType where Cursor.LoadRe
let startIndex = self.count
self.count += min(restOfLoaded, self.pageSize)
return Observable.just(startIndex..<self.count)
return .just(self.cursor[startIndex..<self.count])
}
return self.cursor.loadNextBatch()

View File

@ -22,9 +22,7 @@
import RxSwift
public typealias MapCursorLoadResultType = CountableRange<Int>
public extension CursorType where Self.LoadResultType == MapCursorLoadResultType {
public extension CursorType {
/// Creates MapCursor with current cursor
///
@ -37,9 +35,7 @@ public extension CursorType where Self.LoadResultType == MapCursorLoadResultType
}
/// Map cursor implementation with enclosed cursor for fetching results
public class MapCursor<Cursor: CursorType, T>: CursorType where Cursor.LoadResultType == MapCursorLoadResultType {
public typealias LoadResultType = Cursor.LoadResultType
public class MapCursor<Cursor: CursorType, T>: CursorType {
public typealias Transform = (Cursor.Element) -> T?
@ -73,15 +69,15 @@ public class MapCursor<Cursor: CursorType, T>: CursorType where Cursor.LoadResul
return elements[index]
}
public func loadNextBatch() -> Observable<LoadResultType> {
public func loadNextBatch() -> Observable<[T]> {
return Observable.deferred {
self.semaphore.wait()
return self.cursor.loadNextBatch().map { loadedRange in
let startIndex = self.elements.count
self.elements += self.cursor[loadedRange].flatMap(self.transform)
return self.cursor.loadNextBatch().map { newItems in
let transformedNewItems = newItems.flatMap(self.transform)
self.elements += transformedNewItems
return startIndex..<self.elements.count
return transformedNewItems
}
}
.do(onNext: { [weak semaphore] _ in

View File

@ -25,8 +25,6 @@ import RxSwift
/// Stub cursor implementation for array content type
public class StaticCursor<Element>: CursorType {
public typealias LoadResultType = CountableRange<Int>
private let content: [Element]
private let semaphore = DispatchSemaphore(value: 1)
@ -46,7 +44,7 @@ public class StaticCursor<Element>: CursorType {
return content[index]
}
public func loadNextBatch() -> Observable<LoadResultType> {
public func loadNextBatch() -> Observable<[Element]> {
return Observable.deferred {
self.semaphore.wait()
@ -58,7 +56,7 @@ public class StaticCursor<Element>: CursorType {
self.exhausted = true
return Observable.just(0..<self.count)
return .just(self.content)
}
.do(onNext: { [weak semaphore] _ in
semaphore?.signal()

View File

@ -0,0 +1,53 @@
//
// Copyright (c) 2017 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import UIKit
public protocol PaginationTableViewWrapperDelegate: class {
associatedtype Cursor: ResettableCursorType
func paginationWrapper(wrapper: PaginationTableViewWrapper<Cursor, Self>,
didLoad newItems: [Cursor.Element],
itemsBefore: [Cursor.Element])
func paginationWrapper(wrapper: PaginationTableViewWrapper<Cursor, Self>,
didReload allItems: [Cursor.Element])
}
public class PaginationTableViewWrapper<C: ResettableCursorType, D: PaginationTableViewWrapperDelegate>
where D.Cursor == C {
private let tableView: UITableView
private let paginationViewModel: PaginationViewModel<C>
private weak var delegate: D?
public init(tableView: UITableView, cursor: C, delegate: D) {
self.tableView = tableView
self.paginationViewModel = PaginationViewModel(cursor: cursor)
self.delegate = delegate
}
}

View File

@ -23,8 +23,9 @@
import RxSwift
import RxCocoa
public class PaginationViewModel<C: CursorType>
where C: ResettableCursorType, C.LoadResultType == CountableRange<Int> {
public typealias ResettableCursorType = CursorType & ResettableType
public final class PaginationViewModel<C: ResettableCursorType> {
public indirect enum State {
@ -51,6 +52,8 @@ where C: ResettableCursorType, C.LoadResultType == CountableRange<Int> {
private var currentRequest: Disposable?
private let internalScheduler = SerialDispatchQueueScheduler(qos: .default)
public var state: Driver<State> {
return internalState.asDriver()
}
@ -75,16 +78,15 @@ where C: ResettableCursorType, C.LoadResultType == CountableRange<Int> {
}
currentRequest = cursor.loadNextBatch()
.subscribe(onNext: { [weak self] loadedRange in
self?.onGot(cursorLoadResult: loadedRange)
.subscribeOn(internalScheduler)
.subscribe(onNext: { [weak self] newItems in
self?.onGot(newItems: newItems)
}, onError: { [weak self] error in
self?.onGot(error: error)
})
}
private func onGot(cursorLoadResult: C.LoadResultType) {
let newItems = cursor[cursorLoadResult]
private func onGot(newItems: [C.Element]) {
if newItems.count > 0 {
internalState.value = .results(newItems: newItems, after: internalState.value)
} else {

View File

@ -22,9 +22,13 @@
import Foundation
public extension CursorType where LoadResultType == CountableRange<Int> {
public extension CursorType {
subscript(range: LoadResultType) -> [Self.Element] {
subscript(range: CountableRange<Int>) -> [Self.Element] {
return range.map { self[$0] }
}
subscript(range: CountableClosedRange<Int>) -> [Self.Element] {
return range.map { self[$0] }
}
@ -33,15 +37,3 @@ public extension CursorType where LoadResultType == CountableRange<Int> {
}
}
public extension CursorType where LoadResultType == CountableClosedRange<Int> {
subscript(range: LoadResultType) -> [Self.Element] {
return range.map { self[$0] }
}
var loadedElements: [Self.Element] {
return self[0...count - 1]
}
}

View File

@ -27,8 +27,6 @@ public protocol CursorType {
associatedtype Element
associatedtype LoadResultType
/// Indicates that cursor load all available results
var exhausted: Bool { get }
@ -40,6 +38,6 @@ public protocol CursorType {
/// Loads next batch of results
///
/// - Returns: Observable of LoadResultType
func loadNextBatch() -> Observable<LoadResultType>
func loadNextBatch() -> Observable<[Element]>
}

View File

@ -22,13 +22,13 @@
import Foundation
public protocol ResettableCursorType {
public protocol ResettableType {
init(initialFrom other: Self)
}
public extension ResettableCursorType {
public extension ResettableType {
func reset() -> Self {
return Self(initialFrom: self)

View File

@ -58,8 +58,8 @@ class CursorTests: XCTestCase {
let cursorExpectationError = expectation(description: "Fixed page cursor error expectation")
fixedPageCursor.loadNextBatch()
.subscribe(onNext: { loadedRange in
XCTAssertEqual(fixedPageCursor[loadedRange].count, 15)
.subscribe(onNext: { loadedItems in
XCTAssertEqual(loadedItems.count, 15)
cursorExpectation.fulfill()
})
@ -89,8 +89,8 @@ class CursorTests: XCTestCase {
let cursorExpectation = expectation(description: "Fixed page cursor expectation")
fixedPageCursor.loadNextBatch()
.subscribe(onNext: { loadedRange in
XCTAssertEqual(fixedPageCursor[loadedRange].count, 8)
.subscribe(onNext: { loadedItems in
XCTAssertEqual(loadedItems.count, 8)
cursorExpectation.fulfill()
})

View File

@ -23,7 +23,7 @@
import LeadKit
import RxSwift
class StubCursor: CursorType, ResettableCursorType {
class StubCursor: ResettableCursorType {
typealias LoadResultType = CountableRange<Int>
@ -57,7 +57,7 @@ class StubCursor: CursorType, ResettableCursorType {
self.requestDelay = other.requestDelay
}
func loadNextBatch() -> Observable<CountableRange<Int>> {
func loadNextBatch() -> Observable<[Post]> {
return Observable.create { observer -> Disposable in
if self.exhausted {
observer.onError(CursorError.exhausted)
@ -71,7 +71,7 @@ class StubCursor: CursorType, ResettableCursorType {
self.posts = Array((self.posts + newPosts)[0..<maxNewPosts])
observer.onNext(countBefore..<self.count)
observer.onNext(self[countBefore..<self.count])
observer.onCompleted()
})
}