diff --git a/LeadKit/LeadKit.xcodeproj/project.pbxproj b/LeadKit/LeadKit.xcodeproj/project.pbxproj index 8e109283..3676e1bb 100644 --- a/LeadKit/LeadKit.xcodeproj/project.pbxproj +++ b/LeadKit/LeadKit.xcodeproj/project.pbxproj @@ -21,11 +21,17 @@ 7837F60F1CBCF5C0000D74C1 /* EstimatedViewHeightProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7837F60E1CBCF5C0000D74C1 /* EstimatedViewHeightProtocol.swift */; }; 786D78E81D53C378006B2CEA /* AlamofireRequest+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 786D78E71D53C378006B2CEA /* AlamofireRequest+Extensions.swift */; }; 786D78EC1D53C46E006B2CEA /* AlamofireManager+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 786D78EB1D53C46E006B2CEA /* AlamofireManager+Extensions.swift */; }; + 78753E241DE58A5D006BC0FB /* CursorError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78753E231DE58A5D006BC0FB /* CursorError.swift */; }; + 78753E2C1DE58BF9006BC0FB /* StaticCursor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78753E2B1DE58BF9006BC0FB /* StaticCursor.swift */; }; + 78753E2E1DE58DBA006BC0FB /* FixedPageCursor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78753E2D1DE58DBA006BC0FB /* FixedPageCursor.swift */; }; + 78753E301DE594B4006BC0FB /* MapCursor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78753E2F1DE594B4006BC0FB /* MapCursor.swift */; }; 787682FA1CAD40C300532AB3 /* StaticEstimatedViewHeightProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 787682F91CAD40C200532AB3 /* StaticEstimatedViewHeightProtocol.swift */; }; 787783631CA03CA0001CDC9B /* IndexPath+ImmutableIndexPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 787783621CA03CA0001CDC9B /* IndexPath+ImmutableIndexPath.swift */; }; 787783671CA04D4A001CDC9B /* String+SizeCalculation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 787783661CA04D4A001CDC9B /* String+SizeCalculation.swift */; }; 7884DB9C1DC1439200E52A63 /* UserDefaults+MappableDataTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884DB9B1DC1439200E52A63 /* UserDefaults+MappableDataTypes.swift */; }; 788EC15A1CF64528009CFB6B /* UIStoryboard+InstantiateViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 788EC1591CF64528009CFB6B /* UIStoryboard+InstantiateViewController.swift */; }; + 789CC6081DE5835600F789D3 /* CursorType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 789CC6071DE5835600F789D3 /* CursorType.swift */; }; + 789CC60B1DE584F800F789D3 /* CursorType+Slice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 789CC60A1DE584F800F789D3 /* CursorType+Slice.swift */; }; 78A0FCC71DC366A10070B5E1 /* StoryboardProtocol+DefaultBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78A0FCC51DC366A10070B5E1 /* StoryboardProtocol+DefaultBundle.swift */; }; 78A0FCC81DC366A10070B5E1 /* StoryboardProtocol+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78A0FCC61DC366A10070B5E1 /* StoryboardProtocol+Extensions.swift */; }; 78A74EA91C6B373700FE9724 /* UIView+DefaultNibName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78A74EA81C6B373700FE9724 /* UIView+DefaultNibName.swift */; }; @@ -83,11 +89,17 @@ 7837F60E1CBCF5C0000D74C1 /* EstimatedViewHeightProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EstimatedViewHeightProtocol.swift; sourceTree = ""; }; 786D78E71D53C378006B2CEA /* AlamofireRequest+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AlamofireRequest+Extensions.swift"; sourceTree = ""; }; 786D78EB1D53C46E006B2CEA /* AlamofireManager+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AlamofireManager+Extensions.swift"; sourceTree = ""; }; + 78753E231DE58A5D006BC0FB /* CursorError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CursorError.swift; sourceTree = ""; }; + 78753E2B1DE58BF9006BC0FB /* StaticCursor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StaticCursor.swift; sourceTree = ""; }; + 78753E2D1DE58DBA006BC0FB /* FixedPageCursor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FixedPageCursor.swift; sourceTree = ""; }; + 78753E2F1DE594B4006BC0FB /* MapCursor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MapCursor.swift; sourceTree = ""; }; 787682F91CAD40C200532AB3 /* StaticEstimatedViewHeightProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StaticEstimatedViewHeightProtocol.swift; sourceTree = ""; }; 787783621CA03CA0001CDC9B /* IndexPath+ImmutableIndexPath.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "IndexPath+ImmutableIndexPath.swift"; sourceTree = ""; }; 787783661CA04D4A001CDC9B /* String+SizeCalculation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+SizeCalculation.swift"; sourceTree = ""; }; 7884DB9B1DC1439200E52A63 /* UserDefaults+MappableDataTypes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UserDefaults+MappableDataTypes.swift"; sourceTree = ""; }; 788EC1591CF64528009CFB6B /* UIStoryboard+InstantiateViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIStoryboard+InstantiateViewController.swift"; sourceTree = ""; }; + 789CC6071DE5835600F789D3 /* CursorType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CursorType.swift; sourceTree = ""; }; + 789CC60A1DE584F800F789D3 /* CursorType+Slice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CursorType+Slice.swift"; sourceTree = ""; }; 78A0FCC51DC366A10070B5E1 /* StoryboardProtocol+DefaultBundle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "StoryboardProtocol+DefaultBundle.swift"; sourceTree = ""; }; 78A0FCC61DC366A10070B5E1 /* StoryboardProtocol+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "StoryboardProtocol+Extensions.swift"; sourceTree = ""; }; 78A74EA81C6B373700FE9724 /* UIView+DefaultNibName.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "UIView+DefaultNibName.swift"; path = "LeadKit/Extensions/UIView/UIView+DefaultNibName.swift"; sourceTree = SOURCE_ROOT; }; @@ -164,6 +176,7 @@ 78011A651D47AF3000EA16A2 /* Enums */ = { isa = PBXGroup; children = ( + 78753E231DE58A5D006BC0FB /* CursorError.swift */, ); path = Enums; sourceTree = ""; @@ -214,6 +227,16 @@ path = Alamofire; sourceTree = ""; }; + 78753E2A1DE58BED006BC0FB /* Cursors */ = { + isa = PBXGroup; + children = ( + 78753E2B1DE58BF9006BC0FB /* StaticCursor.swift */, + 78753E2D1DE58DBA006BC0FB /* FixedPageCursor.swift */, + 78753E2F1DE594B4006BC0FB /* MapCursor.swift */, + ); + path = Cursors; + sourceTree = ""; + }; 787783611CA03C84001CDC9B /* IndexPath */ = { isa = PBXGroup; children = ( @@ -239,6 +262,14 @@ path = UserDefaults; sourceTree = ""; }; + 789CC6091DE584C000F789D3 /* CursorType */ = { + isa = PBXGroup; + children = ( + 789CC60A1DE584F800F789D3 /* CursorType+Slice.swift */, + ); + path = CursorType; + sourceTree = ""; + }; 78A0FCC41DC366A10070B5E1 /* StoryboardProtocol */ = { isa = PBXGroup; children = ( @@ -252,6 +283,7 @@ isa = PBXGroup; children = ( 78B0FC7B1C6B2BAE00358B64 /* Logging */, + 78753E2A1DE58BED006BC0FB /* Cursors */, ); path = Classes; sourceTree = ""; @@ -343,6 +375,7 @@ 78A0FCC41DC366A10070B5E1 /* StoryboardProtocol */, 786D78E61D53C355006B2CEA /* Alamofire */, 7884DB9A1DC1432B00E52A63 /* UserDefaults */, + 789CC6091DE584C000F789D3 /* CursorType */, ); path = Extensions; sourceTree = ""; @@ -360,6 +393,7 @@ 787682F91CAD40C200532AB3 /* StaticEstimatedViewHeightProtocol.swift */, 7837F60E1CBCF5C0000D74C1 /* EstimatedViewHeightProtocol.swift */, 783423691DB8D0E100A79643 /* StoryboardProtocol.swift */, + 789CC6071DE5835600F789D3 /* CursorType.swift */, ); path = Protocols; sourceTree = ""; @@ -592,6 +626,9 @@ 7834236A1DB8D0E100A79643 /* StoryboardProtocol.swift in Sources */, 78CFEE521C5C45E500F50370 /* UITableView+CellRegistration.swift in Sources */, 78B0FC7F1C6B2C4D00358B64 /* Log.swift in Sources */, + 78753E2E1DE58DBA006BC0FB /* FixedPageCursor.swift in Sources */, + 789CC60B1DE584F800F789D3 /* CursorType+Slice.swift in Sources */, + 78753E2C1DE58BF9006BC0FB /* StaticCursor.swift in Sources */, 78D4B54A1DA64EAB005B0764 /* Any+TypeName.swift in Sources */, 78CFEE571C5C45E500F50370 /* StaticNibNameProtocol.swift in Sources */, 788EC15A1CF64528009CFB6B /* UIStoryboard+InstantiateViewController.swift in Sources */, @@ -601,6 +638,7 @@ 786D78EC1D53C46E006B2CEA /* AlamofireManager+Extensions.swift in Sources */, 78B0FC811C6B2CD500358B64 /* App.swift in Sources */, 78B036491DA562C30021D5CC /* CGImage+Template.swift in Sources */, + 78753E301DE594B4006BC0FB /* MapCursor.swift in Sources */, 780D23461DA416F80084620D /* CGContext+Initializers.swift in Sources */, 95B39A861D9D51250057BD54 /* String+Localization.swift in Sources */, 78C36F7E1D801E3E00E7EBEA /* Double+Rounding.swift in Sources */, @@ -609,6 +647,7 @@ 78A0FCC81DC366A10070B5E1 /* StoryboardProtocol+Extensions.swift in Sources */, 78B036411DA4D7060021D5CC /* UIImage+Extensions.swift in Sources */, 78A0FCC71DC366A10070B5E1 /* StoryboardProtocol+DefaultBundle.swift in Sources */, + 78753E241DE58A5D006BC0FB /* CursorError.swift in Sources */, 786D78E81D53C378006B2CEA /* AlamofireRequest+Extensions.swift in Sources */, 78C36F811D8021DD00E7EBEA /* UIColor+Hex.swift in Sources */, 78CFEE5B1C5C45E500F50370 /* ViewModelProtocol.swift in Sources */, @@ -621,6 +660,7 @@ 78CFEE581C5C45E500F50370 /* StaticViewHeightProtocol.swift in Sources */, 787783631CA03CA0001CDC9B /* IndexPath+ImmutableIndexPath.swift in Sources */, 78B036471DA5624D0021D5CC /* CGImage+Creation.swift in Sources */, + 789CC6081DE5835600F789D3 /* CursorType.swift in Sources */, 78B0364B1DA61EDE0021D5CC /* CGImage+Crop.swift in Sources */, 78B036451DA561D00021D5CC /* CGImage+Utils.swift in Sources */, 78CFEE591C5C45E500F50370 /* StoryboardIdentifierProtocol.swift in Sources */, diff --git a/LeadKit/LeadKit/Classes/Cursors/FixedPageCursor.swift b/LeadKit/LeadKit/Classes/Cursors/FixedPageCursor.swift new file mode 100644 index 00000000..116b642e --- /dev/null +++ b/LeadKit/LeadKit/Classes/Cursors/FixedPageCursor.swift @@ -0,0 +1,60 @@ +// +// FixedPageCursor.swift +// LeadKit +// +// Created by Ivan Smolin on 23/11/16. +// Copyright © 2016 Touch Instinct. All rights reserved. +// + +import RxSwift + +/// Paging cursor implementation with enclosed cursor for fetching results +public class FixedPageCursor: CursorType where Cursor.LoadResultType == CountableRange { + + public typealias LoadResultType = CountableRange + + private let cursor: Cursor + + private let pageSize: Int + + /// Initializer with enclosed cursor + /// + /// - Parameters: + /// - cursor: enclosed cursor + /// - pageSize: number of items loaded at once + public init(cursor: Cursor, pageSize: Int) { + self.cursor = cursor + self.pageSize = pageSize + } + + public var exhausted: Bool { + return cursor.exhausted && cursor.count == count + } + + public private(set) var count: Int = 0 + + public subscript(index: Int) -> Cursor.Element { + return cursor[index] + } + + public func loadNextBatch() -> Observable { + return Observable.deferred { + if self.exhausted { + throw CursorError.exhausted + } + + let restOfLoaded = self.cursor.count - self.count + + if restOfLoaded >= self.pageSize || self.cursor.exhausted { + let startIndex = self.count + self.count += min(restOfLoaded, self.pageSize) + + return Observable.just(startIndex.. + +public extension CursorType where Self.LoadResultType == MapCursorLoadResultType { + + /// Creates MapCursor with current cursor + /// + /// - Parameter transform: closure to transform elements + /// - Returns: new MapCursorInstance + func flatMap(transform: @escaping MapCursor.Transform) -> MapCursor { + return MapCursor(cursor: self, transform: transform) + } + +} + +/// Map cursor implementation with enclosed cursor for fetching results +public class MapCursor: CursorType where Cursor.LoadResultType == MapCursorLoadResultType { + + public typealias LoadResultType = Cursor.LoadResultType + + public typealias Transform = (Cursor.Element) -> T? + + private let cursor: Cursor + + private let transform: Transform + + private var elements: [T] = [] + + /// Initializer with enclosed cursor + /// + /// - Parameters: + /// - cursor: enclosed cursor + /// - transform: closure to transform elements + public init(cursor: Cursor, transform: @escaping Transform) { + self.cursor = cursor + self.transform = transform + } + + public var exhausted: Bool { + return cursor.exhausted + } + + public var count: Int { + return elements.count + } + + public subscript(index: Int) -> T { + return elements[index] + } + + public func loadNextBatch() -> Observable { + return cursor.loadNextBatch().map { loadedRange in + let startIndex = self.elements.count + self.elements += self.cursor[loadedRange].flatMap(self.transform) + + return startIndex..: CursorType { + + public typealias LoadResultType = CountableRange + + private let content: [Element] + + /// Initializer for array content type + /// + /// - Parameter content: array with elements of Elemet type + public init(content: [Element]) { + self.content = content + } + + public private(set) var exhausted = false + + public private(set) var count = 0 + + public subscript(index: Int) -> Element { + return content[index] + } + + public func loadNextBatch() -> Observable { + return Observable.deferred { + if self.exhausted { + throw CursorError.exhausted + } + + self.count = self.content.count + + self.exhausted = true + + return Observable.just(0.. { + + subscript(range: LoadResultType) -> [Self.Element] { + return range.map { self[$0] } + } + + var loadedElements: [Self.Element] { + return self[0.. { + + 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 new file mode 100644 index 00000000..21dabee2 --- /dev/null +++ b/LeadKit/LeadKit/Protocols/CursorType.swift @@ -0,0 +1,31 @@ +// +// CursorType.swift +// LeadKit +// +// Created by Ivan Smolin on 23/11/16. +// Copyright © 2016 Touch Instinct. All rights reserved. +// + +import RxSwift + +/// Protocol which describes Cursor data type +public protocol CursorType { + + associatedtype Element + + associatedtype LoadResultType + + /// Indicates that cursor load all available results + var exhausted: Bool { get } + + /// Current number of items in cursor + var count: Int { get } + + subscript(index: Int) -> Self.Element { get } + + /// Loads next batch of results + /// + /// - Returns: Observable of LoadResultType + func loadNextBatch() -> Observable + +}