initial PaginationTableViewWrapper. api breaking changes in CursorType protocol
This commit is contained in:
parent
f9fbfb2735
commit
ded59ff2d9
|
|
@ -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 */,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]>
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue