diff --git a/LeadKit/LeadKit.xcodeproj/project.pbxproj b/LeadKit/LeadKit.xcodeproj/project.pbxproj index daeaec34..89f526b3 100644 --- a/LeadKit/LeadKit.xcodeproj/project.pbxproj +++ b/LeadKit/LeadKit.xcodeproj/project.pbxproj @@ -14,6 +14,8 @@ 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 */; }; + 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 */; }; 78011AB31D48B53600EA16A2 /* ApiRequestParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78011AB21D48B53600EA16A2 /* ApiRequestParameters.swift */; }; 780D23431DA412470084620D /* CGImage+Alpha.swift in Sources */ = {isa = PBXBuildFile; fileRef = 780D23421DA412470084620D /* CGImage+Alpha.swift */; }; @@ -102,6 +104,8 @@ 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 = ""; }; + 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 = ""; }; 78011AB21D48B53600EA16A2 /* ApiRequestParameters.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApiRequestParameters.swift; sourceTree = ""; }; 780D23421DA412470084620D /* CGImage+Alpha.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CGImage+Alpha.swift"; sourceTree = ""; }; @@ -231,6 +235,14 @@ path = Views; sourceTree = ""; }; + 67EF144A1E8BEA9C00D6E0DD /* Cursors */ = { + isa = PBXGroup; + children = ( + 67EF144B1E8BEACB00D6E0DD /* StubCursor.swift */, + ); + path = Cursors; + sourceTree = ""; + }; 78011A651D47AF3000EA16A2 /* Enums */ = { isa = PBXGroup; children = ( @@ -449,9 +461,11 @@ children = ( 67B305791E8A8727008169CA /* Views */, 6727419E1E65BF3C0075836A /* Models */, + 67EF144A1E8BEA9C00D6E0DD /* Cursors */, 78CFEE3B1C5C456B00F50370 /* Info.plist */, 6727419C1E65B99E0075836A /* MappableUserDefaultsTests.swift */, 67B3057E1E8A8804008169CA /* LoadFromNibTests.swift */, + 67EF144D1E8BED4E00D6E0DD /* CursorTests.swift */, ); path = LeadKitTests; sourceTree = ""; @@ -910,6 +924,8 @@ buildActionMask = 2147483647; files = ( 67B3057D1E8A8735008169CA /* TestView.swift in Sources */, + 67EF144E1E8BED4E00D6E0DD /* CursorTests.swift in Sources */, + 67EF144C1E8BEACB00D6E0DD /* StubCursor.swift in Sources */, 67B3057F1E8A8804008169CA /* LoadFromNibTests.swift in Sources */, 6727419D1E65B99E0075836A /* MappableUserDefaultsTests.swift in Sources */, 672741A01E65C1E00075836A /* Post.swift in Sources */, diff --git a/LeadKit/LeadKit/Classes/Cursors/FixedPageCursor.swift b/LeadKit/LeadKit/Classes/Cursors/FixedPageCursor.swift index ca64e3de..28025a37 100644 --- a/LeadKit/LeadKit/Classes/Cursors/FixedPageCursor.swift +++ b/LeadKit/LeadKit/Classes/Cursors/FixedPageCursor.swift @@ -31,6 +31,8 @@ public class FixedPageCursor: CursorType where Cursor.LoadRe private let pageSize: Int + private let semaphore = DispatchSemaphore(value: 1) + /// Initializer with enclosed cursor /// /// - Parameters: @@ -52,7 +54,13 @@ public class FixedPageCursor: CursorType where Cursor.LoadRe } public func loadNextBatch() -> Observable { + return loadNextBatch(withSemaphore: semaphore) + } + + private func loadNextBatch(withSemaphore semaphore: DispatchSemaphore?) -> Observable { return Observable.deferred { + semaphore?.wait() + if self.exhausted { throw CursorError.exhausted } @@ -67,8 +75,15 @@ public class FixedPageCursor: CursorType where Cursor.LoadRe } return self.cursor.loadNextBatch() - .flatMap { _ in self.loadNextBatch() } + .flatMap { _ in + self.loadNextBatch(withSemaphore: nil) + } } + .do(onNext: { [weak semaphore] _ in + semaphore?.signal() + }, onError: { [weak semaphore] _ in + semaphore?.signal() + }) } } diff --git a/LeadKit/LeadKit/Classes/Cursors/MapCursor.swift b/LeadKit/LeadKit/Classes/Cursors/MapCursor.swift index 4a994c0c..1f8277b7 100644 --- a/LeadKit/LeadKit/Classes/Cursors/MapCursor.swift +++ b/LeadKit/LeadKit/Classes/Cursors/MapCursor.swift @@ -49,6 +49,8 @@ public class MapCursor: CursorType where Cursor.LoadResul private var elements: [T] = [] + private let semaphore = DispatchSemaphore(value: 1) + /// Initializer with enclosed cursor /// /// - Parameters: @@ -72,12 +74,21 @@ public class MapCursor: CursorType where Cursor.LoadResul } public func loadNextBatch() -> Observable { - return cursor.loadNextBatch().map { loadedRange in - let startIndex = self.elements.count - self.elements += self.cursor[loadedRange].flatMap(self.transform) + return Observable.deferred { + self.semaphore.wait() - return startIndex..: CursorType { private let content: [Element] + private let semaphore = DispatchSemaphore(value: 1) + /// Initializer for array content type /// /// - Parameter content: array with elements of Elemet type @@ -46,6 +48,8 @@ public class StaticCursor: CursorType { public func loadNextBatch() -> Observable { return Observable.deferred { + self.semaphore.wait() + if self.exhausted { throw CursorError.exhausted } @@ -56,6 +60,11 @@ public class StaticCursor: CursorType { return Observable.just(0.. 1 ? nil : $0 } + let fixedPageCursor = FixedPageCursor(cursor: mapCursor, pageSize: 20) + + let cursorExpectation = expectation(description: "Fixed page cursor expectation") + + fixedPageCursor.loadNextBatch() + .subscribe(onNext: { loadedRange in + XCTAssertEqual(fixedPageCursor[loadedRange].count, 8) + + cursorExpectation.fulfill() + }) + .addDisposableTo(disposeBag) + + waitForExpectations(timeout: 10, handler: nil) + } + +} diff --git a/LeadKit/LeadKitTests/Cursors/StubCursor.swift b/LeadKit/LeadKitTests/Cursors/StubCursor.swift new file mode 100644 index 00000000..c0f28b13 --- /dev/null +++ b/LeadKit/LeadKitTests/Cursors/StubCursor.swift @@ -0,0 +1,78 @@ +// +// 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 LeadKit +import RxSwift + +class StubCursor: CursorType { + + typealias LoadResultType = CountableRange + + typealias Element = Post + + private var posts: [Post] = [] + + private let maxItemsCount: Int + + private let requestDelay: DispatchTimeInterval + + var count: Int { + return posts.count + } + + var exhausted: Bool { + return count >= maxItemsCount + } + + subscript(index: Int) -> Post { + return posts[index] + } + + init(maxItemsCount: Int = 12, requestDelay: DispatchTimeInterval = .seconds(2)) { + self.maxItemsCount = maxItemsCount + self.requestDelay = requestDelay + } + + func loadNextBatch() -> Observable> { + return Observable.create { observer -> Disposable in + if self.exhausted { + observer.onError(CursorError.exhausted) + } else { + DispatchQueue.global().asyncAfter(deadline: .now() + self.requestDelay, execute: { + let countBefore = self.count + + let newPosts = Post.generate() + + let maxNewPosts = min(self.maxItemsCount, countBefore + newPosts.count) + + self.posts = Array((self.posts + newPosts)[0.. [Post] { + return [Post(userId: 1, postId: 1, title: "First post", body: ""), + Post(userId: 1, postId: 2, title: "Second post", body: ""), + Post(userId: 2, postId: 3, title: "Third post", body: ""), + Post(userId: 2, postId: 4, title: "Forth post", body: "")] + } + +}