initial pagination view model
This commit is contained in:
parent
908ea53d3e
commit
f9fbfb2735
|
|
@ -9,11 +9,14 @@
|
|||
/* Begin PBXBuildFile section */
|
||||
6727419D1E65B99E0075836A /* MappableUserDefaultsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6727419C1E65B99E0075836A /* MappableUserDefaultsTests.swift */; };
|
||||
672741A01E65C1E00075836A /* Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6727419F1E65C1E00075836A /* Post.swift */; };
|
||||
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 */; };
|
||||
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 */; };
|
||||
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 */; };
|
||||
|
|
@ -99,11 +102,14 @@
|
|||
12F36034A5278991B658B53E /* Pods_LeadKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_LeadKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
6727419C1E65B99E0075836A /* MappableUserDefaultsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MappableUserDefaultsTests.swift; sourceTree = "<group>"; };
|
||||
6727419F1E65C1E00075836A /* Post.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Post.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
|
@ -210,6 +216,14 @@
|
|||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
675D24B01E9234A400E92D1F /* Pagination */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
675D24B11E9234BB00E92D1F /* PaginationViewModel.swift */,
|
||||
);
|
||||
path = Pagination;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
67788F9D1E6965F800484DEE /* CGFloat */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -391,6 +405,7 @@
|
|||
78B0FC7B1C6B2BAE00358B64 /* Logging */,
|
||||
78753E2A1DE58BED006BC0FB /* Cursors */,
|
||||
67B305801E8A92B6008169CA /* Views */,
|
||||
675D24B01E9234A400E92D1F /* Pagination */,
|
||||
);
|
||||
path = Classes;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -466,6 +481,7 @@
|
|||
6727419C1E65B99E0075836A /* MappableUserDefaultsTests.swift */,
|
||||
67B3057E1E8A8804008169CA /* LoadFromNibTests.swift */,
|
||||
67EF144D1E8BED4E00D6E0DD /* CursorTests.swift */,
|
||||
674743931E929A5A00B47671 /* PaginationViewModelTests.swift */,
|
||||
);
|
||||
path = LeadKitTests;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -517,6 +533,7 @@
|
|||
783423691DB8D0E100A79643 /* StoryboardProtocol.swift */,
|
||||
78CFEE4F1C5C45E500F50370 /* ViewHeightProtocol.swift */,
|
||||
78CFEE501C5C45E500F50370 /* ViewModelProtocol.swift */,
|
||||
67B856E21E923BE600F54304 /* ResettableCursorType.swift */,
|
||||
);
|
||||
path = Protocols;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -871,6 +888,7 @@
|
|||
78011A641D47ABC500EA16A2 /* UIView+DefaultReuseIdentifier.swift in Sources */,
|
||||
786D78EC1D53C46E006B2CEA /* AlamofireManager+Extensions.swift in Sources */,
|
||||
CAA707D51E2E614E0022D732 /* ModuleConfigurator.swift in Sources */,
|
||||
675D24B21E9234BB00E92D1F /* PaginationViewModel.swift in Sources */,
|
||||
78B0FC811C6B2CD500358B64 /* App.swift in Sources */,
|
||||
78B036491DA562C30021D5CC /* CGImage+Template.swift in Sources */,
|
||||
CAE698C61E96775F000394B0 /* String+Extensions.swift in Sources */,
|
||||
|
|
@ -907,6 +925,7 @@
|
|||
CAA707D71E2E616D0022D732 /* BaseViewModel.swift in Sources */,
|
||||
E126CBB31DB68DDA00E1B2F8 /* UICollectionView+CellRegistration.swift in Sources */,
|
||||
78CFEE581C5C45E500F50370 /* StaticViewHeightProtocol.swift in Sources */,
|
||||
67B856E31E923BE600F54304 /* ResettableCursorType.swift in Sources */,
|
||||
787783631CA03CA0001CDC9B /* IndexPath+ImmutableIndexPath.swift in Sources */,
|
||||
78B036471DA5624D0021D5CC /* CGImage+Creation.swift in Sources */,
|
||||
789CC6081DE5835600F789D3 /* CursorType.swift in Sources */,
|
||||
|
|
@ -927,6 +946,7 @@
|
|||
67EF144E1E8BED4E00D6E0DD /* CursorTests.swift in Sources */,
|
||||
67EF144C1E8BEACB00D6E0DD /* StubCursor.swift in Sources */,
|
||||
67B3057F1E8A8804008169CA /* LoadFromNibTests.swift in Sources */,
|
||||
674743941E929A5A00B47671 /* PaginationViewModelTests.swift in Sources */,
|
||||
6727419D1E65B99E0075836A /* MappableUserDefaultsTests.swift in Sources */,
|
||||
672741A01E65C1E00075836A /* Post.swift in Sources */,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,103 @@
|
|||
//
|
||||
// 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 RxSwift
|
||||
import RxCocoa
|
||||
|
||||
public class PaginationViewModel<C: CursorType>
|
||||
where C: ResettableCursorType, C.LoadResultType == CountableRange<Int> {
|
||||
|
||||
public indirect enum State {
|
||||
|
||||
case initial
|
||||
case loading // can be after any state
|
||||
case loadingMore // can be after results
|
||||
case results(newItems: [C.Element], after: State) // can be after loading or loadingMore
|
||||
case error(error: Error, after: State) // can be after loading or loadingMore
|
||||
case empty // can be after loading or loadingMore
|
||||
case exhausted // can be after results
|
||||
|
||||
}
|
||||
|
||||
public enum LoadType {
|
||||
|
||||
case reload
|
||||
case next
|
||||
|
||||
}
|
||||
|
||||
private var cursor: C
|
||||
|
||||
private let internalState = Variable<State>(.initial)
|
||||
|
||||
private var currentRequest: Disposable?
|
||||
|
||||
public var state: Driver<State> {
|
||||
return internalState.asDriver()
|
||||
}
|
||||
|
||||
public init(cursor: C) {
|
||||
self.cursor = cursor
|
||||
}
|
||||
|
||||
public func load(_ loadType: LoadType) {
|
||||
switch loadType {
|
||||
case .reload:
|
||||
currentRequest?.dispose()
|
||||
cursor = cursor.reset()
|
||||
|
||||
internalState.value = .loading
|
||||
case .next:
|
||||
if case .exhausted(_) = internalState.value {
|
||||
preconditionFailure("You shouldn't call load(.next) after got .exhausted state!")
|
||||
}
|
||||
|
||||
internalState.value = .loadingMore
|
||||
}
|
||||
|
||||
currentRequest = cursor.loadNextBatch()
|
||||
.subscribe(onNext: { [weak self] loadedRange in
|
||||
self?.onGot(cursorLoadResult: loadedRange)
|
||||
}, onError: { [weak self] error in
|
||||
self?.onGot(error: error)
|
||||
})
|
||||
}
|
||||
|
||||
private func onGot(cursorLoadResult: C.LoadResultType) {
|
||||
let newItems = cursor[cursorLoadResult]
|
||||
|
||||
if newItems.count > 0 {
|
||||
internalState.value = .results(newItems: newItems, after: internalState.value)
|
||||
} else {
|
||||
internalState.value = .empty
|
||||
}
|
||||
|
||||
if cursor.exhausted {
|
||||
internalState.value = .exhausted
|
||||
}
|
||||
}
|
||||
|
||||
private func onGot(error: Error) {
|
||||
internalState.value = .error(error: error, after: internalState.value)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -24,11 +24,9 @@ import Foundation
|
|||
|
||||
/// A type representing an possible errors that can be thrown during working with cursor object
|
||||
///
|
||||
/// - busy: cursor is currently processing another request
|
||||
/// - exhausted: cursor did load all available results
|
||||
public enum CursorError: Error {
|
||||
|
||||
case busy
|
||||
case exhausted
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
//
|
||||
// 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 Foundation
|
||||
|
||||
public protocol ResettableCursorType {
|
||||
|
||||
init(initialFrom other: Self)
|
||||
|
||||
}
|
||||
|
||||
public extension ResettableCursorType {
|
||||
|
||||
func reset() -> Self {
|
||||
return Self(initialFrom: self)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,9 +1,23 @@
|
|||
//
|
||||
// CursorTests.swift
|
||||
// LeadKit
|
||||
// Copyright (c) 2017 Touch Instinct
|
||||
//
|
||||
// Created by Ivan Smolin on 29/03/2017.
|
||||
// Copyright © 2017 Touch Instinct. All rights reserved.
|
||||
// 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 XCTest
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
import LeadKit
|
||||
import RxSwift
|
||||
|
||||
class StubCursor: CursorType {
|
||||
class StubCursor: CursorType, ResettableCursorType {
|
||||
|
||||
typealias LoadResultType = CountableRange<Int>
|
||||
|
||||
|
|
@ -52,6 +52,11 @@ class StubCursor: CursorType {
|
|||
self.requestDelay = requestDelay
|
||||
}
|
||||
|
||||
required init(initialFrom other: StubCursor) {
|
||||
self.maxItemsCount = other.maxItemsCount
|
||||
self.requestDelay = other.requestDelay
|
||||
}
|
||||
|
||||
func loadNextBatch() -> Observable<CountableRange<Int>> {
|
||||
return Observable.create { observer -> Disposable in
|
||||
if self.exhausted {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
//
|
||||
// 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 XCTest
|
||||
import LeadKit
|
||||
import RxSwift
|
||||
|
||||
class PaginationViewModelTests: XCTestCase {
|
||||
|
||||
let disposeBag = DisposeBag()
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testExample() {
|
||||
let cursor = StubCursor(maxItemsCount: 36, requestDelay: .seconds(1))
|
||||
let viewModel = PaginationViewModel(cursor: cursor)
|
||||
|
||||
let paginationExpectation = expectation(description: "Pagination expectation")
|
||||
|
||||
viewModel.state.drive(onNext: { state in
|
||||
switch state {
|
||||
case .initial, .loadingMore, .loading:
|
||||
print("PageViewModel state changed to \(state)")
|
||||
case .results(let newItems, _):
|
||||
print(newItems.count)
|
||||
paginationExpectation.fulfill()
|
||||
default:
|
||||
XCTFail("Unexpected state: \(state)")
|
||||
}
|
||||
})
|
||||
.addDisposableTo(disposeBag)
|
||||
|
||||
viewModel.load(.reload)
|
||||
|
||||
waitForExpectations(timeout: 10, handler: nil)
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue