diff --git a/LeadKit/LeadKit.xcodeproj/project.pbxproj b/LeadKit/LeadKit.xcodeproj/project.pbxproj index 72d94e62..bff7ba8c 100644 --- a/LeadKit/LeadKit.xcodeproj/project.pbxproj +++ b/LeadKit/LeadKit.xcodeproj/project.pbxproj @@ -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 = ""; }; 675D24B11E9234BB00E92D1F /* PaginationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaginationViewModel.swift; sourceTree = ""; }; 67788F9E1E69661800484DEE /* CGFloat+Pixels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CGFloat+Pixels.swift"; sourceTree = ""; }; + 678A20291E93C1A900787562 /* PaginationTableViewWrapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaginationTableViewWrapper.swift; sourceTree = ""; }; 67B3057A1E8A8727008169CA /* TestView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = TestView.xib; sourceTree = ""; }; 67B3057C1E8A8735008169CA /* TestView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestView.swift; sourceTree = ""; }; 67B3057E1E8A8804008169CA /* LoadFromNibTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadFromNibTests.swift; sourceTree = ""; }; 67B305831E8A92E8008169CA /* XibView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XibView.swift; sourceTree = ""; }; - 67B856E21E923BE600F54304 /* ResettableCursorType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResettableCursorType.swift; sourceTree = ""; }; + 67B856E21E923BE600F54304 /* ResettableType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResettableType.swift; sourceTree = ""; }; 67EF144B1E8BEACB00D6E0DD /* StubCursor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StubCursor.swift; sourceTree = ""; }; 67EF144D1E8BED4E00D6E0DD /* CursorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CursorTests.swift; sourceTree = ""; }; 78011A631D47ABC500EA16A2 /* UIView+DefaultReuseIdentifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+DefaultReuseIdentifier.swift"; sourceTree = ""; }; @@ -220,6 +222,7 @@ isa = PBXGroup; children = ( 675D24B11E9234BB00E92D1F /* PaginationViewModel.swift */, + 678A20291E93C1A900787562 /* PaginationTableViewWrapper.swift */, ); path = Pagination; sourceTree = ""; @@ -533,7 +536,7 @@ 783423691DB8D0E100A79643 /* StoryboardProtocol.swift */, 78CFEE4F1C5C45E500F50370 /* ViewHeightProtocol.swift */, 78CFEE501C5C45E500F50370 /* ViewModelProtocol.swift */, - 67B856E21E923BE600F54304 /* ResettableCursorType.swift */, + 67B856E21E923BE600F54304 /* ResettableType.swift */, ); path = Protocols; sourceTree = ""; @@ -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 */, diff --git a/LeadKit/LeadKit/Classes/Cursors/FixedPageCursor.swift b/LeadKit/LeadKit/Classes/Cursors/FixedPageCursor.swift index 28025a37..bdbf4829 100644 --- a/LeadKit/LeadKit/Classes/Cursors/FixedPageCursor.swift +++ b/LeadKit/LeadKit/Classes/Cursors/FixedPageCursor.swift @@ -23,9 +23,7 @@ import RxSwift /// Paging cursor implementation with enclosed cursor for fetching results -public class FixedPageCursor: CursorType where Cursor.LoadResultType == CountableRange { - - public typealias LoadResultType = CountableRange +public class FixedPageCursor: CursorType { private let cursor: Cursor @@ -53,11 +51,11 @@ public class FixedPageCursor: CursorType where Cursor.LoadRe return cursor[index] } - public func loadNextBatch() -> Observable { + public func loadNextBatch() -> Observable<[Cursor.Element]> { return loadNextBatch(withSemaphore: semaphore) } - private func loadNextBatch(withSemaphore semaphore: DispatchSemaphore?) -> Observable { + private func loadNextBatch(withSemaphore semaphore: DispatchSemaphore?) -> Observable<[Cursor.Element]> { return Observable.deferred { semaphore?.wait() @@ -71,7 +69,7 @@ public class FixedPageCursor: CursorType where Cursor.LoadRe let startIndex = self.count self.count += min(restOfLoaded, self.pageSize) - return Observable.just(startIndex.. - -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: CursorType where Cursor.LoadResultType == MapCursorLoadResultType { - - public typealias LoadResultType = Cursor.LoadResultType +public class MapCursor: CursorType { public typealias Transform = (Cursor.Element) -> T? @@ -73,15 +69,15 @@ public class MapCursor: CursorType where Cursor.LoadResul return elements[index] } - public func loadNextBatch() -> Observable { + 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..: CursorType { - public typealias LoadResultType = CountableRange - private let content: [Element] private let semaphore = DispatchSemaphore(value: 1) @@ -46,7 +44,7 @@ public class StaticCursor: CursorType { return content[index] } - public func loadNextBatch() -> Observable { + public func loadNextBatch() -> Observable<[Element]> { return Observable.deferred { self.semaphore.wait() @@ -58,7 +56,7 @@ public class StaticCursor: CursorType { self.exhausted = true - return Observable.just(0.., + didLoad newItems: [Cursor.Element], + itemsBefore: [Cursor.Element]) + + func paginationWrapper(wrapper: PaginationTableViewWrapper, + didReload allItems: [Cursor.Element]) + +} + +public class PaginationTableViewWrapper +where D.Cursor == C { + + private let tableView: UITableView + + private let paginationViewModel: PaginationViewModel + + private weak var delegate: D? + + public init(tableView: UITableView, cursor: C, delegate: D) { + self.tableView = tableView + self.paginationViewModel = PaginationViewModel(cursor: cursor) + self.delegate = delegate + } + +} diff --git a/LeadKit/LeadKit/Classes/Pagination/PaginationViewModel.swift b/LeadKit/LeadKit/Classes/Pagination/PaginationViewModel.swift index 5fc3fe56..ca66af7e 100644 --- a/LeadKit/LeadKit/Classes/Pagination/PaginationViewModel.swift +++ b/LeadKit/LeadKit/Classes/Pagination/PaginationViewModel.swift @@ -23,8 +23,9 @@ import RxSwift import RxCocoa -public class PaginationViewModel -where C: ResettableCursorType, C.LoadResultType == CountableRange { +public typealias ResettableCursorType = CursorType & ResettableType + +public final class PaginationViewModel { public indirect enum State { @@ -51,6 +52,8 @@ where C: ResettableCursorType, C.LoadResultType == CountableRange { private var currentRequest: Disposable? + private let internalScheduler = SerialDispatchQueueScheduler(qos: .default) + public var state: Driver { return internalState.asDriver() } @@ -75,16 +78,15 @@ where C: ResettableCursorType, C.LoadResultType == CountableRange { } 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 { diff --git a/LeadKit/LeadKit/Extensions/CursorType/CursorType+Slice.swift b/LeadKit/LeadKit/Extensions/CursorType/CursorType+Slice.swift index 7c403514..dcc7d94d 100644 --- a/LeadKit/LeadKit/Extensions/CursorType/CursorType+Slice.swift +++ b/LeadKit/LeadKit/Extensions/CursorType/CursorType+Slice.swift @@ -22,9 +22,13 @@ import Foundation -public extension CursorType where LoadResultType == CountableRange { +public extension CursorType { - subscript(range: LoadResultType) -> [Self.Element] { + subscript(range: CountableRange) -> [Self.Element] { + return range.map { self[$0] } + } + + subscript(range: CountableClosedRange) -> [Self.Element] { return range.map { self[$0] } } @@ -33,15 +37,3 @@ public extension CursorType where LoadResultType == CountableRange { } } - -public extension CursorType where LoadResultType == CountableClosedRange { - - subscript(range: LoadResultType) -> [Self.Element] { - return range.map { self[$0] } - } - - var loadedElements: [Self.Element] { - return self[0...count - 1] - } - -} diff --git a/LeadKit/LeadKit/Protocols/CursorType.swift b/LeadKit/LeadKit/Protocols/CursorType.swift index 554ebfc8..3de50906 100644 --- a/LeadKit/LeadKit/Protocols/CursorType.swift +++ b/LeadKit/LeadKit/Protocols/CursorType.swift @@ -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 + func loadNextBatch() -> Observable<[Element]> } diff --git a/LeadKit/LeadKit/Protocols/ResettableCursorType.swift b/LeadKit/LeadKit/Protocols/ResettableType.swift similarity index 94% rename from LeadKit/LeadKit/Protocols/ResettableCursorType.swift rename to LeadKit/LeadKit/Protocols/ResettableType.swift index 939e7ee9..fefe1b2b 100644 --- a/LeadKit/LeadKit/Protocols/ResettableCursorType.swift +++ b/LeadKit/LeadKit/Protocols/ResettableType.swift @@ -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) diff --git a/LeadKit/LeadKitTests/CursorTests.swift b/LeadKit/LeadKitTests/CursorTests.swift index 39b2f59c..e03519b2 100644 --- a/LeadKit/LeadKitTests/CursorTests.swift +++ b/LeadKit/LeadKitTests/CursorTests.swift @@ -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() }) diff --git a/LeadKit/LeadKitTests/Cursors/StubCursor.swift b/LeadKit/LeadKitTests/Cursors/StubCursor.swift index 1fe4f17e..4d391635 100644 --- a/LeadKit/LeadKitTests/Cursors/StubCursor.swift +++ b/LeadKit/LeadKitTests/Cursors/StubCursor.swift @@ -23,7 +23,7 @@ import LeadKit import RxSwift -class StubCursor: CursorType, ResettableCursorType { +class StubCursor: ResettableCursorType { typealias LoadResultType = CountableRange @@ -57,7 +57,7 @@ class StubCursor: CursorType, ResettableCursorType { self.requestDelay = other.requestDelay } - func loadNextBatch() -> Observable> { + 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..