commit
9b73354cc0
|
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = "LeadKit"
|
||||
s.version = "0.4.9"
|
||||
s.version = "0.5.0"
|
||||
s.summary = "iOS framework with a bunch of tools for rapid development"
|
||||
s.homepage = "https://github.com/TouchInstinct/LeadKit"
|
||||
s.license = "Apache License, Version 2.0"
|
||||
|
|
@ -16,4 +16,5 @@ Pod::Spec.new do |s|
|
|||
s.dependency "ObjectMapper", '~> 2.1'
|
||||
s.dependency "Toast-Swift", '~> 2.0.0'
|
||||
s.dependency "TableKit", '~> 2.3.1'
|
||||
s.dependency "UIScrollView-InfiniteScroll", '~> 1.0.0'
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9,11 +9,25 @@
|
|||
/* 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 */; };
|
||||
675FB4251EA7797C0075BF3D /* Mutex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 675FB4241EA7797C0075BF3D /* Mutex.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 */; };
|
||||
679DE4901E9588B6006F25FE /* SupportProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 679DE48F1E9588B6006F25FE /* SupportProtocol.swift */; };
|
||||
679DE4941E9613ED006F25FE /* UIScrollView+Support.swift in Sources */ = {isa = PBXBuildFile; fileRef = 679DE4931E9613ED006F25FE /* UIScrollView+Support.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 /* ResettableType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67B856E21E923BE600F54304 /* ResettableType.swift */; };
|
||||
67DC65041E979B34002F2FFF /* LoadingIndicatorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67DC65031E979B34002F2FFF /* LoadingIndicatorProtocol.swift */; };
|
||||
67DC65061E979B70002F2FFF /* UIView+LoadingIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67DC65051E979B70002F2FFF /* UIView+LoadingIndicator.swift */; };
|
||||
67DC65091E979BB8002F2FFF /* UIActivityIndicator+LoadingIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67DC65081E979BB8002F2FFF /* UIActivityIndicator+LoadingIndicator.swift */; };
|
||||
67DC650C1E979C0A002F2FFF /* AnyLoadingIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67DC650B1E979C0A002F2FFF /* AnyLoadingIndicator.swift */; };
|
||||
67DC650F1E979D0C002F2FFF /* PaginationTableViewWrapperDelegate+DefaultImplementation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67DC650E1E979D0C002F2FFF /* PaginationTableViewWrapperDelegate+DefaultImplementation.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 */; };
|
||||
|
|
@ -43,7 +57,7 @@
|
|||
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 */; };
|
||||
78A74EA91C6B373700FE9724 /* UIView+DefaultXibName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78A74EA81C6B373700FE9724 /* UIView+DefaultXibName.swift */; };
|
||||
78B036411DA4D7060021D5CC /* UIImage+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78B036401DA4D7060021D5CC /* UIImage+Extensions.swift */; };
|
||||
78B036431DA4FEC90021D5CC /* CGImage+Transform.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78B036421DA4FEC90021D5CC /* CGImage+Transform.swift */; };
|
||||
78B036451DA561D00021D5CC /* CGImage+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78B036441DA561D00021D5CC /* CGImage+Utils.swift */; };
|
||||
|
|
@ -60,7 +74,7 @@
|
|||
78CFEE351C5C456B00F50370 /* LeadKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 78CFEE2A1C5C456B00F50370 /* LeadKit.framework */; };
|
||||
78CFEE541C5C45E500F50370 /* UIView+LoadFromNib.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78CFEE481C5C45E500F50370 /* UIView+LoadFromNib.swift */; };
|
||||
78CFEE561C5C45E500F50370 /* ReuseIdentifierProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78CFEE4B1C5C45E500F50370 /* ReuseIdentifierProtocol.swift */; };
|
||||
78CFEE571C5C45E500F50370 /* StaticNibNameProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78CFEE4C1C5C45E500F50370 /* StaticNibNameProtocol.swift */; };
|
||||
78CFEE571C5C45E500F50370 /* XibNameProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78CFEE4C1C5C45E500F50370 /* XibNameProtocol.swift */; };
|
||||
78CFEE581C5C45E500F50370 /* StaticViewHeightProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78CFEE4D1C5C45E500F50370 /* StaticViewHeightProtocol.swift */; };
|
||||
78CFEE591C5C45E500F50370 /* StoryboardIdentifierProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78CFEE4E1C5C45E500F50370 /* StoryboardIdentifierProtocol.swift */; };
|
||||
78CFEE5A1C5C45E500F50370 /* ViewHeightProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78CFEE4F1C5C45E500F50370 /* ViewHeightProtocol.swift */; };
|
||||
|
|
@ -78,7 +92,7 @@
|
|||
CAE698C21E965B47000394B0 /* TableDirector+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAE698C01E965B47000394B0 /* TableDirector+Extensions.swift */; };
|
||||
CAE698C61E96775F000394B0 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAE698C41E96775F000394B0 /* String+Extensions.swift */; };
|
||||
E126CBB31DB68DDA00E1B2F8 /* UICollectionView+CellRegistration.swift in Sources */ = {isa = PBXBuildFile; fileRef = E126CBB21DB68DDA00E1B2F8 /* UICollectionView+CellRegistration.swift */; };
|
||||
EDF3DE3F1EA4F2E80016F729 /* UIViewController+XibName.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDF3DE3E1EA4F2E80016F729 /* UIViewController+XibName.swift */; };
|
||||
EDF3DE3F1EA4F2E80016F729 /* UIViewController+DefaultXibName.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDF3DE3E1EA4F2E80016F729 /* UIViewController+DefaultXibName.swift */; };
|
||||
EF2921A61E165DF400E8F43B /* TimeInterval+DateComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF2921A51E165DF400E8F43B /* TimeInterval+DateComponents.swift */; };
|
||||
EF5FB5691E0141610030E4BE /* UIView+Rotation.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF5FB5681E0141610030E4BE /* UIView+Rotation.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
|
@ -95,13 +109,28 @@
|
|||
|
||||
/* Begin PBXFileReference section */
|
||||
12F36034A5278991B658B53E /* Pods_LeadKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_LeadKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
671FF1611EAA264B001B882C /* iOS.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = iOS.playground; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
675FB4241EA7797C0075BF3D /* Mutex.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Mutex.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>"; };
|
||||
679DE48F1E9588B6006F25FE /* SupportProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SupportProtocol.swift; sourceTree = "<group>"; };
|
||||
679DE4931E9613ED006F25FE /* UIScrollView+Support.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIScrollView+Support.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 /* ResettableType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResettableType.swift; sourceTree = "<group>"; };
|
||||
67DC65031E979B34002F2FFF /* LoadingIndicatorProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadingIndicatorProtocol.swift; sourceTree = "<group>"; };
|
||||
67DC65051E979B70002F2FFF /* UIView+LoadingIndicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+LoadingIndicator.swift"; sourceTree = "<group>"; };
|
||||
67DC65081E979BB8002F2FFF /* UIActivityIndicator+LoadingIndicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIActivityIndicator+LoadingIndicator.swift"; sourceTree = "<group>"; };
|
||||
67DC650B1E979C0A002F2FFF /* AnyLoadingIndicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnyLoadingIndicator.swift; sourceTree = "<group>"; };
|
||||
67DC650E1E979D0C002F2FFF /* PaginationTableViewWrapperDelegate+DefaultImplementation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "PaginationTableViewWrapperDelegate+DefaultImplementation.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>"; };
|
||||
78011AB21D48B53600EA16A2 /* ApiRequestParameters.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApiRequestParameters.swift; sourceTree = "<group>"; };
|
||||
780D23421DA412470084620D /* CGImage+Alpha.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CGImage+Alpha.swift"; sourceTree = "<group>"; };
|
||||
|
|
@ -131,7 +160,7 @@
|
|||
789CC60A1DE584F800F789D3 /* CursorType+Slice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CursorType+Slice.swift"; sourceTree = "<group>"; };
|
||||
78A0FCC51DC366A10070B5E1 /* StoryboardProtocol+DefaultBundle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "StoryboardProtocol+DefaultBundle.swift"; sourceTree = "<group>"; };
|
||||
78A0FCC61DC366A10070B5E1 /* StoryboardProtocol+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "StoryboardProtocol+Extensions.swift"; sourceTree = "<group>"; };
|
||||
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; };
|
||||
78A74EA81C6B373700FE9724 /* UIView+DefaultXibName.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "UIView+DefaultXibName.swift"; path = "LeadKit/Extensions/UIView/UIView+DefaultXibName.swift"; sourceTree = SOURCE_ROOT; };
|
||||
78B036401DA4D7060021D5CC /* UIImage+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIImage+Extensions.swift"; sourceTree = "<group>"; };
|
||||
78B036421DA4FEC90021D5CC /* CGImage+Transform.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CGImage+Transform.swift"; sourceTree = "<group>"; };
|
||||
78B036441DA561D00021D5CC /* CGImage+Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CGImage+Utils.swift"; sourceTree = "<group>"; };
|
||||
|
|
@ -151,7 +180,7 @@
|
|||
78CFEE3B1C5C456B00F50370 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
78CFEE481C5C45E500F50370 /* UIView+LoadFromNib.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "UIView+LoadFromNib.swift"; path = "LeadKit/Extensions/UIView/UIView+LoadFromNib.swift"; sourceTree = SOURCE_ROOT; };
|
||||
78CFEE4B1C5C45E500F50370 /* ReuseIdentifierProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReuseIdentifierProtocol.swift; sourceTree = "<group>"; };
|
||||
78CFEE4C1C5C45E500F50370 /* StaticNibNameProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StaticNibNameProtocol.swift; sourceTree = "<group>"; };
|
||||
78CFEE4C1C5C45E500F50370 /* XibNameProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XibNameProtocol.swift; sourceTree = "<group>"; };
|
||||
78CFEE4D1C5C45E500F50370 /* StaticViewHeightProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StaticViewHeightProtocol.swift; sourceTree = "<group>"; };
|
||||
78CFEE4E1C5C45E500F50370 /* StoryboardIdentifierProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StoryboardIdentifierProtocol.swift; sourceTree = "<group>"; };
|
||||
78CFEE4F1C5C45E500F50370 /* ViewHeightProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewHeightProtocol.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -172,7 +201,7 @@
|
|||
CAE698C41E96775F000394B0 /* String+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = "<group>"; };
|
||||
CC832342120EAD568C9F7FC3 /* Pods-LeadKitTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LeadKitTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-LeadKitTests/Pods-LeadKitTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
E126CBB21DB68DDA00E1B2F8 /* UICollectionView+CellRegistration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "UICollectionView+CellRegistration.swift"; path = "UICollectionView/UICollectionView+CellRegistration.swift"; sourceTree = "<group>"; };
|
||||
EDF3DE3E1EA4F2E80016F729 /* UIViewController+XibName.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIViewController+XibName.swift"; sourceTree = "<group>"; };
|
||||
EDF3DE3E1EA4F2E80016F729 /* UIViewController+DefaultXibName.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIViewController+DefaultXibName.swift"; sourceTree = "<group>"; };
|
||||
EF2921A51E165DF400E8F43B /* TimeInterval+DateComponents.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "TimeInterval+DateComponents.swift"; sourceTree = "<group>"; };
|
||||
EF5FB5681E0141610030E4BE /* UIView+Rotation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+Rotation.swift"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
|
@ -206,6 +235,23 @@
|
|||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
675D24B01E9234A400E92D1F /* Pagination */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
675D24B11E9234BB00E92D1F /* PaginationViewModel.swift */,
|
||||
678A20291E93C1A900787562 /* PaginationTableViewWrapper.swift */,
|
||||
);
|
||||
path = Pagination;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
675FB4231EA779650075BF3D /* Concurrency */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
675FB4241EA7797C0075BF3D /* Mutex.swift */,
|
||||
);
|
||||
path = Concurrency;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
67788F9D1E6965F800484DEE /* CGFloat */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -214,6 +260,14 @@
|
|||
path = CGFloat;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
679DE4921E9613ED006F25FE /* Support */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
679DE4931E9613ED006F25FE /* UIScrollView+Support.swift */,
|
||||
);
|
||||
path = Support;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
67B305791E8A8727008169CA /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -231,6 +285,38 @@
|
|||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
67DC65071E979BA9002F2FFF /* UIActivityIndicator */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
67DC65081E979BB8002F2FFF /* UIActivityIndicator+LoadingIndicator.swift */,
|
||||
);
|
||||
path = UIActivityIndicator;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
67DC650A1E979BFD002F2FFF /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
67DC650B1E979C0A002F2FFF /* AnyLoadingIndicator.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
67DC650D1E979CF7002F2FFF /* PaginationTableViewWrapperDelegate */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
67DC650E1E979D0C002F2FFF /* PaginationTableViewWrapperDelegate+DefaultImplementation.swift */,
|
||||
);
|
||||
path = PaginationTableViewWrapperDelegate;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
67EF144A1E8BEA9C00D6E0DD /* Cursors */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
67EF144B1E8BEACB00D6E0DD /* StubCursor.swift */,
|
||||
);
|
||||
path = Cursors;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
78011A651D47AF3000EA16A2 /* Enums */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -243,6 +329,7 @@
|
|||
78011AAE1D48B46100EA16A2 /* Structures */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
67DC650A1E979BFD002F2FFF /* Views */,
|
||||
78011AB11D48B53600EA16A2 /* Api */,
|
||||
);
|
||||
path = Structures;
|
||||
|
|
@ -379,6 +466,8 @@
|
|||
78B0FC7B1C6B2BAE00358B64 /* Logging */,
|
||||
78753E2A1DE58BED006BC0FB /* Cursors */,
|
||||
67B305801E8A92B6008169CA /* Views */,
|
||||
675D24B01E9234A400E92D1F /* Pagination */,
|
||||
675FB4231EA779650075BF3D /* Concurrency */,
|
||||
);
|
||||
path = Classes;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -440,6 +529,7 @@
|
|||
78D4B54B1DA650FC005B0764 /* Functions */,
|
||||
78CFEE2D1C5C456B00F50370 /* LeadKit.h */,
|
||||
78CFEE2F1C5C456B00F50370 /* Info.plist */,
|
||||
671FF1611EAA264B001B882C /* iOS.playground */,
|
||||
);
|
||||
path = LeadKit;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -449,9 +539,12 @@
|
|||
children = (
|
||||
67B305791E8A8727008169CA /* Views */,
|
||||
6727419E1E65BF3C0075836A /* Models */,
|
||||
67EF144A1E8BEA9C00D6E0DD /* Cursors */,
|
||||
78CFEE3B1C5C456B00F50370 /* Info.plist */,
|
||||
6727419C1E65B99E0075836A /* MappableUserDefaultsTests.swift */,
|
||||
67B3057E1E8A8804008169CA /* LoadFromNibTests.swift */,
|
||||
67EF144D1E8BED4E00D6E0DD /* CursorTests.swift */,
|
||||
674743931E929A5A00B47671 /* PaginationViewModelTests.swift */,
|
||||
);
|
||||
path = LeadKitTests;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -468,11 +561,14 @@
|
|||
787783611CA03C84001CDC9B /* IndexPath */,
|
||||
787D87481E10E19000D6015C /* ObjectMapper */,
|
||||
787609201E1403460093CE36 /* Observable */,
|
||||
67DC650D1E979CF7002F2FFF /* PaginationTableViewWrapperDelegate */,
|
||||
780F56C81E0D76A5004530B6 /* Sequence */,
|
||||
78A0FCC41DC366A10070B5E1 /* StoryboardProtocol */,
|
||||
787783651CA04D14001CDC9B /* String */,
|
||||
CAE698BF1E965AE9000394B0 /* TableDirector */,
|
||||
679DE4921E9613ED006F25FE /* Support */,
|
||||
EF2921A41E16595100E8F43B /* TimeInterval */,
|
||||
67DC65071E979BA9002F2FFF /* UIActivityIndicator */,
|
||||
E126CBB11DB68D9A00E1B2F8 /* UICollectionView */,
|
||||
78C36F7F1D8021D100E7EBEA /* UIColor */,
|
||||
CA1FE7071E27D79C00968901 /* UIDevice */,
|
||||
|
|
@ -497,12 +593,15 @@
|
|||
780F56CB1E0D7ACA004530B6 /* ObservableMappable.swift */,
|
||||
78CFEE4B1C5C45E500F50370 /* ReuseIdentifierProtocol.swift */,
|
||||
787682F91CAD40C200532AB3 /* StaticEstimatedViewHeightProtocol.swift */,
|
||||
78CFEE4C1C5C45E500F50370 /* StaticNibNameProtocol.swift */,
|
||||
78CFEE4C1C5C45E500F50370 /* XibNameProtocol.swift */,
|
||||
78CFEE4D1C5C45E500F50370 /* StaticViewHeightProtocol.swift */,
|
||||
78CFEE4E1C5C45E500F50370 /* StoryboardIdentifierProtocol.swift */,
|
||||
783423691DB8D0E100A79643 /* StoryboardProtocol.swift */,
|
||||
78CFEE4F1C5C45E500F50370 /* ViewHeightProtocol.swift */,
|
||||
78CFEE501C5C45E500F50370 /* ViewModelProtocol.swift */,
|
||||
67B856E21E923BE600F54304 /* ResettableType.swift */,
|
||||
679DE48F1E9588B6006F25FE /* SupportProtocol.swift */,
|
||||
67DC65031E979B34002F2FFF /* LoadingIndicatorProtocol.swift */,
|
||||
);
|
||||
path = Protocols;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -512,7 +611,7 @@
|
|||
children = (
|
||||
78D4B5451DA64D49005B0764 /* UIViewController+DefaultStoryboardIdentifier.swift */,
|
||||
78C54AFC1E432EEF0051EFBA /* UIViewController+TopVisibleViewController.swift */,
|
||||
EDF3DE3E1EA4F2E80016F729 /* UIViewController+XibName.swift */,
|
||||
EDF3DE3E1EA4F2E80016F729 /* UIViewController+DefaultXibName.swift */,
|
||||
);
|
||||
path = UIViewController;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -529,10 +628,11 @@
|
|||
78E59B2C1C786CD500C6BFE9 /* UIView */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
78A74EA81C6B373700FE9724 /* UIView+DefaultNibName.swift */,
|
||||
78A74EA81C6B373700FE9724 /* UIView+DefaultXibName.swift */,
|
||||
78011A631D47ABC500EA16A2 /* UIView+DefaultReuseIdentifier.swift */,
|
||||
78CFEE481C5C45E500F50370 /* UIView+LoadFromNib.swift */,
|
||||
EF5FB5681E0141610030E4BE /* UIView+Rotation.swift */,
|
||||
67DC65051E979B70002F2FFF /* UIView+LoadingIndicator.swift */,
|
||||
);
|
||||
path = UIView;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -849,22 +949,28 @@
|
|||
789CC60B1DE584F800F789D3 /* CursorType+Slice.swift in Sources */,
|
||||
78753E2C1DE58BF9006BC0FB /* StaticCursor.swift in Sources */,
|
||||
78D4B54A1DA64EAB005B0764 /* Any+TypeName.swift in Sources */,
|
||||
78CFEE571C5C45E500F50370 /* StaticNibNameProtocol.swift in Sources */,
|
||||
78CFEE571C5C45E500F50370 /* XibNameProtocol.swift in Sources */,
|
||||
67DC650C1E979C0A002F2FFF /* AnyLoadingIndicator.swift in Sources */,
|
||||
788EC15A1CF64528009CFB6B /* UIStoryboard+InstantiateViewController.swift in Sources */,
|
||||
787783671CA04D4A001CDC9B /* String+SizeCalculation.swift in Sources */,
|
||||
7873D1511E112B0D001816EB /* Any+Cast.swift in Sources */,
|
||||
78B036431DA4FEC90021D5CC /* CGImage+Transform.swift in Sources */,
|
||||
78011A641D47ABC500EA16A2 /* UIView+DefaultReuseIdentifier.swift in Sources */,
|
||||
678A202A1E93C1A900787562 /* PaginationTableViewWrapper.swift in Sources */,
|
||||
679DE4901E9588B6006F25FE /* SupportProtocol.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 */,
|
||||
7873D14F1E1127BC001816EB /* LeadKitError.swift in Sources */,
|
||||
78753E301DE594B4006BC0FB /* MapCursor.swift in Sources */,
|
||||
679DE4941E9613ED006F25FE /* UIScrollView+Support.swift in Sources */,
|
||||
780D23461DA416F80084620D /* CGContext+Initializers.swift in Sources */,
|
||||
95B39A861D9D51250057BD54 /* String+Localization.swift in Sources */,
|
||||
78C36F7E1D801E3E00E7EBEA /* Double+Rounding.swift in Sources */,
|
||||
67DC65061E979B70002F2FFF /* UIView+LoadingIndicator.swift in Sources */,
|
||||
787609221E1403830093CE36 /* Observable+DeferredJust.swift in Sources */,
|
||||
67B305841E8A92E8008169CA /* XibView.swift in Sources */,
|
||||
78C54AFD1E432EEF0051EFBA /* UIViewController+TopVisibleViewController.swift in Sources */,
|
||||
|
|
@ -886,18 +992,23 @@
|
|||
78CFEE5A1C5C45E500F50370 /* ViewHeightProtocol.swift in Sources */,
|
||||
787682FA1CAD40C300532AB3 /* StaticEstimatedViewHeightProtocol.swift in Sources */,
|
||||
CA1FE7091E27D7DE00968901 /* UIDevice+Extensions.swift in Sources */,
|
||||
78A74EA91C6B373700FE9724 /* UIView+DefaultNibName.swift in Sources */,
|
||||
78A74EA91C6B373700FE9724 /* UIView+DefaultXibName.swift in Sources */,
|
||||
CAE698C21E965B47000394B0 /* TableDirector+Extensions.swift in Sources */,
|
||||
CAA707D91E2E61A50022D732 /* ConfigurableController.swift in Sources */,
|
||||
7884DB9C1DC1439200E52A63 /* UserDefaults+MappableDataTypes.swift in Sources */,
|
||||
CAA707D71E2E616D0022D732 /* BaseViewModel.swift in Sources */,
|
||||
E126CBB31DB68DDA00E1B2F8 /* UICollectionView+CellRegistration.swift in Sources */,
|
||||
78CFEE581C5C45E500F50370 /* StaticViewHeightProtocol.swift in Sources */,
|
||||
67DC65091E979BB8002F2FFF /* UIActivityIndicator+LoadingIndicator.swift in Sources */,
|
||||
67B856E31E923BE600F54304 /* ResettableType.swift in Sources */,
|
||||
787783631CA03CA0001CDC9B /* IndexPath+ImmutableIndexPath.swift in Sources */,
|
||||
675FB4251EA7797C0075BF3D /* Mutex.swift in Sources */,
|
||||
78B036471DA5624D0021D5CC /* CGImage+Creation.swift in Sources */,
|
||||
789CC6081DE5835600F789D3 /* CursorType.swift in Sources */,
|
||||
67DC650F1E979D0C002F2FFF /* PaginationTableViewWrapperDelegate+DefaultImplementation.swift in Sources */,
|
||||
67DC65041E979B34002F2FFF /* LoadingIndicatorProtocol.swift in Sources */,
|
||||
78B0364B1DA61EDE0021D5CC /* CGImage+Crop.swift in Sources */,
|
||||
EDF3DE3F1EA4F2E80016F729 /* UIViewController+XibName.swift in Sources */,
|
||||
EDF3DE3F1EA4F2E80016F729 /* UIViewController+DefaultXibName.swift in Sources */,
|
||||
78B036451DA561D00021D5CC /* CGImage+Utils.swift in Sources */,
|
||||
78CFEE591C5C45E500F50370 /* StoryboardIdentifierProtocol.swift in Sources */,
|
||||
78011AB31D48B53600EA16A2 /* ApiRequestParameters.swift in Sources */,
|
||||
|
|
@ -910,7 +1021,10 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
67B3057D1E8A8735008169CA /* TestView.swift in Sources */,
|
||||
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 */,
|
||||
);
|
||||
|
|
@ -1030,12 +1144,6 @@
|
|||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
DYLIB_CURRENT_VERSION = 1;
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/LeadKit",
|
||||
"$(PROJECT_DIR)/LeadKit/Frameworks",
|
||||
"$(PROJECT_DIR)/../Carthage/Build/iOS",
|
||||
);
|
||||
INFOPLIST_FILE = LeadKit/Info.plist;
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
|
|
@ -1056,12 +1164,6 @@
|
|||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
DYLIB_CURRENT_VERSION = 1;
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/LeadKit",
|
||||
"$(PROJECT_DIR)/LeadKit/Frameworks",
|
||||
"$(PROJECT_DIR)/../Carthage/Build/iOS",
|
||||
);
|
||||
INFOPLIST_FILE = LeadKit/Info.plist;
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,92 @@
|
|||
//
|
||||
// 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.
|
||||
//
|
||||
|
||||
// Source https://github.com/mattgallagher/CwlUtils/blob/master/Sources/CwlUtils/CwlMutex.swift
|
||||
final class Mutex {
|
||||
|
||||
// Non-recursive "PTHREAD_MUTEX_NORMAL" and recursive "PTHREAD_MUTEX_RECURSIVE" mutex types.
|
||||
enum MutexType {
|
||||
case normal
|
||||
case recursive
|
||||
}
|
||||
|
||||
private var mutex = pthread_mutex_t()
|
||||
|
||||
/// Default constructs as ".Normal" or ".Recursive" on request.
|
||||
init(type: MutexType = .normal) {
|
||||
var attr = pthread_mutexattr_t()
|
||||
|
||||
guard pthread_mutexattr_init(&attr) == 0 else {
|
||||
preconditionFailure("Failed to init mutex attributes!")
|
||||
}
|
||||
|
||||
switch type {
|
||||
case .normal:
|
||||
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL)
|
||||
case .recursive:
|
||||
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE)
|
||||
}
|
||||
|
||||
guard pthread_mutex_init(&mutex, &attr) == 0 else {
|
||||
preconditionFailure("Failed to init mutex!")
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
pthread_mutex_destroy(&mutex)
|
||||
}
|
||||
|
||||
func sync<R>(execute work: () throws -> R) rethrows -> R {
|
||||
unbalancedLock()
|
||||
|
||||
defer {
|
||||
unbalancedUnlock()
|
||||
}
|
||||
|
||||
return try work()
|
||||
}
|
||||
|
||||
func trySync<R>(execute work: () throws -> R) rethrows -> R? {
|
||||
guard unbalancedTryLock() else {
|
||||
return nil
|
||||
}
|
||||
|
||||
defer {
|
||||
unbalancedUnlock()
|
||||
}
|
||||
|
||||
return try work()
|
||||
}
|
||||
|
||||
func unbalancedLock() {
|
||||
pthread_mutex_lock(&mutex)
|
||||
}
|
||||
|
||||
func unbalancedTryLock() -> Bool {
|
||||
return pthread_mutex_trylock(&mutex) == 0
|
||||
}
|
||||
|
||||
func unbalancedUnlock() {
|
||||
pthread_mutex_unlock(&mutex)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -23,13 +23,11 @@
|
|||
import RxSwift
|
||||
|
||||
/// Paging cursor implementation with enclosed cursor for fetching results
|
||||
public class FixedPageCursor<Cursor: CursorType>: CursorType where Cursor.LoadResultType == CountableRange<Int> {
|
||||
public class FixedPageCursor<Cursor: CursorType>: CursorType {
|
||||
|
||||
public typealias LoadResultType = CountableRange<Int>
|
||||
fileprivate let cursor: Cursor
|
||||
|
||||
private let cursor: Cursor
|
||||
|
||||
private let pageSize: Int
|
||||
fileprivate let pageSize: Int
|
||||
|
||||
/// Initializer with enclosed cursor
|
||||
///
|
||||
|
|
@ -51,7 +49,7 @@ public class FixedPageCursor<Cursor: CursorType>: CursorType where Cursor.LoadRe
|
|||
return cursor[index]
|
||||
}
|
||||
|
||||
public func loadNextBatch() -> Observable<LoadResultType> {
|
||||
public func loadNextBatch() -> Observable<[Cursor.Element]> {
|
||||
return Observable.deferred {
|
||||
if self.exhausted {
|
||||
throw CursorError.exhausted
|
||||
|
|
@ -63,12 +61,27 @@ 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()
|
||||
.flatMap { _ in self.loadNextBatch() }
|
||||
.flatMap { _ in
|
||||
self.loadNextBatch()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// FixedPageCursor subclass with implementation of ResettableType
|
||||
public class ResettableFixedPageCursor<Cursor: ResettableCursorType>: FixedPageCursor<Cursor>, ResettableType {
|
||||
|
||||
public override init(cursor: Cursor, pageSize: Int) {
|
||||
super.init(cursor: cursor, pageSize: pageSize)
|
||||
}
|
||||
|
||||
public required init(initialFrom other: ResettableFixedPageCursor) {
|
||||
super.init(cursor: other.cursor.reset(), pageSize: other.pageSize)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,30 +22,36 @@
|
|||
|
||||
import RxSwift
|
||||
|
||||
public typealias MapCursorLoadResultType = CountableRange<Int>
|
||||
|
||||
public extension CursorType where Self.LoadResultType == MapCursorLoadResultType {
|
||||
public extension CursorType {
|
||||
|
||||
/// Creates MapCursor with current cursor
|
||||
///
|
||||
/// - Parameter transform: closure to transform elements
|
||||
/// - Returns: new MapCursorInstance
|
||||
/// - Returns: new MapCursor instance
|
||||
func flatMap<T>(transform: @escaping MapCursor<Self, T>.Transform) -> MapCursor<Self, T> {
|
||||
return MapCursor(cursor: self, transform: transform)
|
||||
}
|
||||
|
||||
/// Creates ResettableMapCursor with current cursor
|
||||
///
|
||||
/// - Parameter transform: closure to transform elements
|
||||
/// - Returns: new ResettableMapCursor instance
|
||||
func flatMap<T>(transform: @escaping ResettableMapCursor<Self, T>.Transform)
|
||||
-> ResettableMapCursor<Self, T> where Self: ResettableCursorType {
|
||||
|
||||
return ResettableMapCursor(cursor: self, transform: transform)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// 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?
|
||||
|
||||
private let cursor: Cursor
|
||||
fileprivate let cursor: Cursor
|
||||
|
||||
private let transform: Transform
|
||||
fileprivate let transform: Transform
|
||||
|
||||
private var elements: [T] = []
|
||||
|
||||
|
|
@ -71,13 +77,26 @@ public class MapCursor<Cursor: CursorType, T>: CursorType where Cursor.LoadResul
|
|||
return elements[index]
|
||||
}
|
||||
|
||||
public func loadNextBatch() -> Observable<LoadResultType> {
|
||||
return cursor.loadNextBatch().map { loadedRange in
|
||||
let startIndex = self.elements.count
|
||||
self.elements += self.cursor[loadedRange].flatMap(self.transform)
|
||||
public func loadNextBatch() -> Observable<[T]> {
|
||||
return cursor.loadNextBatch().map { newItems in
|
||||
let transformedNewItems = newItems.flatMap(self.transform)
|
||||
self.elements += transformedNewItems
|
||||
|
||||
return startIndex..<self.elements.count
|
||||
return transformedNewItems
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// MapCursor subclass with implementation of ResettableType
|
||||
public class ResettableMapCursor<Cursor: ResettableCursorType, T>: MapCursor<Cursor, T>, ResettableType {
|
||||
|
||||
public override init(cursor: Cursor, transform: @escaping Transform) {
|
||||
super.init(cursor: cursor, transform: transform)
|
||||
}
|
||||
|
||||
public required init(initialFrom other: ResettableMapCursor) {
|
||||
super.init(cursor: other.cursor.reset(), transform: other.transform)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,9 +23,7 @@
|
|||
import RxSwift
|
||||
|
||||
/// Stub cursor implementation for array content type
|
||||
public class StaticCursor<Element>: CursorType {
|
||||
|
||||
public typealias LoadResultType = CountableRange<Int>
|
||||
public class StaticCursor<Element>: ResettableCursorType {
|
||||
|
||||
private let content: [Element]
|
||||
|
||||
|
|
@ -36,6 +34,10 @@ public class StaticCursor<Element>: CursorType {
|
|||
self.content = content
|
||||
}
|
||||
|
||||
public required init(initialFrom other: StaticCursor) {
|
||||
self.content = other.content
|
||||
}
|
||||
|
||||
public private(set) var exhausted = false
|
||||
|
||||
public private(set) var count = 0
|
||||
|
|
@ -44,7 +46,7 @@ public class StaticCursor<Element>: CursorType {
|
|||
return content[index]
|
||||
}
|
||||
|
||||
public func loadNextBatch() -> Observable<LoadResultType> {
|
||||
public func loadNextBatch() -> Observable<[Element]> {
|
||||
return Observable.deferred {
|
||||
if self.exhausted {
|
||||
throw CursorError.exhausted
|
||||
|
|
@ -54,7 +56,7 @@ public class StaticCursor<Element>: CursorType {
|
|||
|
||||
self.exhausted = true
|
||||
|
||||
return Observable.just(0..<self.count)
|
||||
return .just(self.content)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,368 @@
|
|||
//
|
||||
// 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
|
||||
import RxSwift
|
||||
import RxCocoa
|
||||
import UIScrollView_InfiniteScroll
|
||||
|
||||
/// PaginationTableViewWrapper delegate used for pagination results handling and
|
||||
/// customization of bound states (loading, empty, error, etc.).
|
||||
public protocol PaginationTableViewWrapperDelegate: class {
|
||||
|
||||
associatedtype Cursor: ResettableCursorType
|
||||
|
||||
/// Delegate method that handles loading new chunk of data.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - wrapper: Wrapper object that loaded new items.
|
||||
/// - newItems: New items.
|
||||
/// - cursor: Cursor used to load items
|
||||
func paginationWrapper(wrapper: PaginationTableViewWrapper<Cursor, Self>,
|
||||
didLoad newItems: [Cursor.Element],
|
||||
usingCursor cursor: Cursor)
|
||||
|
||||
/// Delegate method that handles reloading or initial loading of data.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - wrapper: Wrapper object that reload items.
|
||||
/// - allItems: New items.
|
||||
/// - cursor: Cursor used to load items
|
||||
func paginationWrapper(wrapper: PaginationTableViewWrapper<Cursor, Self>,
|
||||
didReload allItems: [Cursor.Element],
|
||||
usingCursor cursor: Cursor)
|
||||
|
||||
/// Delegate method that returns placeholder view for empty state.
|
||||
///
|
||||
/// - Parameter wrapper: Wrapper object that requests empty placeholder view.
|
||||
/// - Returns: Configured instace of UIView.
|
||||
func emptyPlaceholder(forPaginationWrapper wrapper: PaginationTableViewWrapper<Cursor, Self>) -> UIView
|
||||
|
||||
/// Delegate method that returns placeholder view for error state.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - wrapper: Wrapper object that requests error placeholder view.
|
||||
/// - error: Error that occured due data loading.
|
||||
/// - Returns: Configured instace of UIView.
|
||||
func errorPlaceholder(forPaginationWrapper wrapper: PaginationTableViewWrapper<Cursor, Self>,
|
||||
forError error: Error) -> UIView
|
||||
|
||||
/// Delegate method that returns loading idicator for initial loading state.
|
||||
/// This indicator will appear at center of the placeholders container.
|
||||
///
|
||||
/// - Parameter wrapper: Wrapper object that requests loading indicator
|
||||
/// - Returns: Configured instace of AnyLoadingIndicator.
|
||||
func initialLoadingIndicator(forPaginationWrapper wrapper: PaginationTableViewWrapper<Cursor, Self>) -> AnyLoadingIndicator
|
||||
|
||||
/// Delegate method that returns loading idicator for initial loading state.
|
||||
///
|
||||
/// - Parameter wrapper: Wrapper object that requests loading indicator.
|
||||
/// - Returns: Configured instace of AnyLoadingIndicator.
|
||||
func loadingMoreIndicator(forPaginationWrapper wrapper: PaginationTableViewWrapper<Cursor, Self>) -> AnyLoadingIndicator
|
||||
|
||||
/// Delegate method that returns instance of UIButton for "retry load more" action.
|
||||
///
|
||||
/// - Parameter wrapper: Wrapper object that requests button for "retry load more" action.
|
||||
/// - Returns: Configured instace of AnyLoadingIndicator.
|
||||
func retryLoadMoreButton(forPaginationWrapper wrapper: PaginationTableViewWrapper<Cursor, Self>) -> UIButton
|
||||
|
||||
/// Delegate method that returns preferred height for "retry load more" button.
|
||||
///
|
||||
/// - Parameter wrapper: Wrapper object that requests height "retry load more" button.
|
||||
/// - Returns: Preferred height of "retry load more" button.
|
||||
func retryLoadMoreButtonHeight(forPaginationWrapper wrapper: PaginationTableViewWrapper<Cursor, Self>) -> CGFloat
|
||||
|
||||
}
|
||||
|
||||
/// Class that connects PaginationViewModel with UITableView. It handles all non-visual and visual states.
|
||||
final public class PaginationTableViewWrapper<Cursor: ResettableCursorType, Delegate: PaginationTableViewWrapperDelegate>
|
||||
where Delegate.Cursor == Cursor {
|
||||
|
||||
private let tableView: UITableView
|
||||
private let paginationViewModel: PaginationViewModel<Cursor>
|
||||
private weak var delegate: Delegate?
|
||||
|
||||
/// Sets the offset between the real end of the scroll view content and the scroll position,
|
||||
/// so the handler can be triggered before reaching end. Defaults to 0.0;
|
||||
public var infiniteScrollTriggerOffset: CGFloat {
|
||||
get {
|
||||
return tableView.infiniteScrollTriggerOffset
|
||||
}
|
||||
set {
|
||||
tableView.infiniteScrollTriggerOffset = newValue
|
||||
}
|
||||
}
|
||||
|
||||
private let disposeBag = DisposeBag()
|
||||
|
||||
private var currentPlaceholderView: UIView?
|
||||
private var currentPlaceholderViewTopConstraint: NSLayoutConstraint?
|
||||
|
||||
private let applicationCurrentyActive = Variable<Bool>(true)
|
||||
|
||||
/// Initializer with table view, placeholders container view, cusor and delegate parameters.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - tableView: UITableView instance to work with.
|
||||
/// - cursor: Cursor object that acts as data source.
|
||||
/// - delegate: Delegate object for data loading events handling and UI customization.
|
||||
public init(tableView: UITableView, cursor: Cursor, delegate: Delegate) {
|
||||
self.tableView = tableView
|
||||
self.paginationViewModel = PaginationViewModel(cursor: cursor)
|
||||
self.delegate = delegate
|
||||
|
||||
bindViewModelStates()
|
||||
|
||||
createRefreshControl()
|
||||
|
||||
bindAppStateNotifications()
|
||||
}
|
||||
|
||||
/// Method that reload all data in internal view model.
|
||||
public func reload() {
|
||||
paginationViewModel.load(.reload)
|
||||
}
|
||||
|
||||
/// Method that enables placeholders animation due pull-to-refresh interaction.
|
||||
///
|
||||
/// - Parameter scrollObservable: Observable that emits content offset as CGPoint.
|
||||
public func setScrollObservable(_ scrollObservable: Observable<CGPoint>) {
|
||||
scrollObservable.subscribe(onNext: { [weak self] offset in
|
||||
self?.currentPlaceholderViewTopConstraint?.constant = -offset.y
|
||||
})
|
||||
.addDisposableTo(disposeBag)
|
||||
}
|
||||
|
||||
// MARK: States handling
|
||||
|
||||
private func onInitialState() {
|
||||
//
|
||||
}
|
||||
|
||||
private func onLoadingState(afterState: PaginationViewModel<Cursor>.State) {
|
||||
if case .initial = afterState {
|
||||
tableView.isUserInteractionEnabled = false
|
||||
|
||||
removeCurrentPlaceholderView()
|
||||
|
||||
guard let loadingIndicator = delegate?.initialLoadingIndicator(forPaginationWrapper: self) else {
|
||||
return
|
||||
}
|
||||
|
||||
let loadingIndicatorView = loadingIndicator.view
|
||||
|
||||
loadingIndicatorView.translatesAutoresizingMaskIntoConstraints = true
|
||||
|
||||
tableView.backgroundView = loadingIndicatorView
|
||||
|
||||
loadingIndicator.startAnimating()
|
||||
|
||||
currentPlaceholderView = loadingIndicatorView
|
||||
} else {
|
||||
tableView.finishInfiniteScroll()
|
||||
tableView.tableFooterView = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func onLoadingMoreState(afterState: PaginationViewModel<Cursor>.State) {
|
||||
if case .error = afterState { // user tap retry button in table footer
|
||||
tableView.tableFooterView = nil
|
||||
addInfiniteScroll()
|
||||
tableView.beginInfiniteScroll(true)
|
||||
}
|
||||
}
|
||||
|
||||
private func onResultsState(newItems: [Cursor.Element],
|
||||
inCursor cursor: Cursor,
|
||||
afterState: PaginationViewModel<Cursor>.State) {
|
||||
|
||||
tableView.isUserInteractionEnabled = true
|
||||
|
||||
if case .loading = afterState {
|
||||
delegate?.paginationWrapper(wrapper: self, didReload: newItems, usingCursor: cursor)
|
||||
|
||||
removeCurrentPlaceholderView()
|
||||
|
||||
tableView.support.refreshControl?.endRefreshing()
|
||||
|
||||
addInfiniteScroll()
|
||||
} else if case .loadingMore = afterState {
|
||||
delegate?.paginationWrapper(wrapper: self, didLoad: newItems, usingCursor: cursor)
|
||||
|
||||
tableView.finishInfiniteScroll()
|
||||
}
|
||||
}
|
||||
|
||||
private func onErrorState(error: Error, afterState: PaginationViewModel<Cursor>.State) {
|
||||
if case .loading = afterState {
|
||||
enterPlaceholderState()
|
||||
|
||||
guard let errorView = delegate?.errorPlaceholder(forPaginationWrapper: self, forError: error) else {
|
||||
return
|
||||
}
|
||||
|
||||
preparePlaceholderView(errorView)
|
||||
|
||||
currentPlaceholderView = errorView
|
||||
} else if case .loadingMore = afterState {
|
||||
tableView.finishInfiniteScroll()
|
||||
|
||||
tableView.removeInfiniteScroll()
|
||||
|
||||
guard let retryButton = delegate?.retryLoadMoreButton(forPaginationWrapper: self),
|
||||
let retryButtonHeigth = delegate?.retryLoadMoreButtonHeight(forPaginationWrapper: self) else {
|
||||
return
|
||||
}
|
||||
|
||||
retryButton.frame = CGRect(x: 0, y: 0, width: tableView.bounds.width, height: retryButtonHeigth)
|
||||
|
||||
retryButton.rx.controlEvent(.touchUpInside)
|
||||
.bindNext { [weak self] in
|
||||
self?.paginationViewModel.load(.next)
|
||||
}
|
||||
.addDisposableTo(disposeBag)
|
||||
|
||||
tableView.tableFooterView = retryButton
|
||||
}
|
||||
}
|
||||
|
||||
private func onEmptyState() {
|
||||
enterPlaceholderState()
|
||||
|
||||
guard let emptyView = delegate?.emptyPlaceholder(forPaginationWrapper: self) else {
|
||||
return
|
||||
}
|
||||
|
||||
preparePlaceholderView(emptyView)
|
||||
|
||||
currentPlaceholderView = emptyView
|
||||
}
|
||||
|
||||
// MARK: private stuff
|
||||
|
||||
private func onExhaustedState() {
|
||||
tableView.removeInfiniteScroll()
|
||||
}
|
||||
|
||||
private func addInfiniteScroll() {
|
||||
tableView.addInfiniteScroll { [weak paginationViewModel] _ in
|
||||
paginationViewModel?.load(.next)
|
||||
}
|
||||
|
||||
tableView.infiniteScrollIndicatorView = delegate?.loadingMoreIndicator(forPaginationWrapper: self).view
|
||||
}
|
||||
|
||||
private func createRefreshControl() {
|
||||
let refreshControl = UIRefreshControl()
|
||||
refreshControl.rx.controlEvent(.valueChanged)
|
||||
.bindNext { [weak self] in
|
||||
self?.reload()
|
||||
}
|
||||
.addDisposableTo(disposeBag)
|
||||
|
||||
tableView.support.setRefreshControl(refreshControl)
|
||||
}
|
||||
|
||||
private func bindViewModelStates() {
|
||||
typealias State = PaginationViewModel<Cursor>.State
|
||||
|
||||
paginationViewModel.state.flatMapLatest { [applicationCurrentyActive] state -> Driver<State> in
|
||||
if applicationCurrentyActive.value {
|
||||
return .just(state)
|
||||
} else {
|
||||
return applicationCurrentyActive
|
||||
.asObservable()
|
||||
.filter { $0 }
|
||||
.delay(0.5, scheduler: MainScheduler.instance)
|
||||
.asDriver(onErrorJustReturn: true)
|
||||
.map { _ in state }
|
||||
}
|
||||
}
|
||||
.drive(onNext: { [weak self] state in
|
||||
switch state {
|
||||
case .initial:
|
||||
self?.onInitialState()
|
||||
case .loading(let after):
|
||||
self?.onLoadingState(afterState: after)
|
||||
case .loadingMore(let after):
|
||||
self?.onLoadingMoreState(afterState: after)
|
||||
case .results(let newItems, let cursor, let after):
|
||||
self?.onResultsState(newItems: newItems, inCursor: cursor, afterState: after)
|
||||
case .error(let error, let after):
|
||||
self?.onErrorState(error: error, afterState: after)
|
||||
case .empty:
|
||||
self?.onEmptyState()
|
||||
case .exhausted:
|
||||
self?.onExhaustedState()
|
||||
}
|
||||
})
|
||||
.addDisposableTo(disposeBag)
|
||||
}
|
||||
|
||||
private func enterPlaceholderState() {
|
||||
tableView.support.refreshControl?.endRefreshing()
|
||||
tableView.isUserInteractionEnabled = true
|
||||
|
||||
removeCurrentPlaceholderView()
|
||||
}
|
||||
|
||||
private func preparePlaceholderView(_ placeholderView: UIView) {
|
||||
placeholderView.translatesAutoresizingMaskIntoConstraints = false
|
||||
placeholderView.isHidden = false
|
||||
|
||||
// I was unable to add pull-to-refresh placeholder scroll behaviour without this trick
|
||||
let wrapperView = UIView()
|
||||
wrapperView.addSubview(placeholderView)
|
||||
|
||||
let leadingConstraint = placeholderView.leadingAnchor.constraint(equalTo: wrapperView.leadingAnchor)
|
||||
let trailingConstraint = placeholderView.trailingAnchor.constraint(equalTo: wrapperView.trailingAnchor)
|
||||
let topConstraint = placeholderView.topAnchor.constraint(equalTo: wrapperView.topAnchor)
|
||||
let bottomConstraint = placeholderView.bottomAnchor.constraint(equalTo: wrapperView.bottomAnchor)
|
||||
|
||||
wrapperView.addConstraints([leadingConstraint, trailingConstraint, topConstraint, bottomConstraint])
|
||||
|
||||
currentPlaceholderViewTopConstraint = topConstraint
|
||||
|
||||
tableView.backgroundView = wrapperView
|
||||
}
|
||||
|
||||
private func removeCurrentPlaceholderView() {
|
||||
tableView.backgroundView = nil
|
||||
}
|
||||
|
||||
private func bindAppStateNotifications() {
|
||||
let notificationCenter = NotificationCenter.default.rx
|
||||
|
||||
notificationCenter.notification(.UIApplicationWillResignActive)
|
||||
.subscribe(onNext: { [weak self] _ in
|
||||
self?.applicationCurrentyActive.value = false
|
||||
})
|
||||
.addDisposableTo(disposeBag)
|
||||
|
||||
notificationCenter.notification(.UIApplicationDidBecomeActive)
|
||||
.subscribe(onNext: { [weak self] _ in
|
||||
self?.applicationCurrentyActive.value = true
|
||||
})
|
||||
.addDisposableTo(disposeBag)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
//
|
||||
// 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
|
||||
|
||||
/// Cursor type which can be resetted
|
||||
public typealias ResettableCursorType = CursorType & ResettableType
|
||||
|
||||
/// Class that encapsulate all pagination logic
|
||||
public final class PaginationViewModel<C: ResettableCursorType> {
|
||||
|
||||
/// Enum contains all possible states for PaginationViewModel class.
|
||||
///
|
||||
/// - initial: initial state of view model.
|
||||
/// Can occur only once after initial binding.
|
||||
/// - loading: loading state of view model. Contains previous state of view model.
|
||||
/// Can occur after any state.
|
||||
/// - loadingMore: loading more items state of view model. Contains previous state of view model.
|
||||
/// Can occur after error or results state.
|
||||
/// - results: results state of view model. Contains loaded items, cursor and previous state of view model.
|
||||
/// Can occur after loading or loadingMore state.
|
||||
/// - error: error state of view model. Contains received error and previous state of view model.
|
||||
/// Can occur after loading or loadingMore state.
|
||||
/// - empty: empty state of view model.
|
||||
/// Can occur after loading or loadingMore state when we got empty result (zero items).
|
||||
/// - exhausted: exhausted state of view model.
|
||||
/// Can occur after results state or after initial->loading state when cursor reports that it's exhausted.
|
||||
public indirect enum State {
|
||||
|
||||
case initial
|
||||
case loading(after: State)
|
||||
case loadingMore(after: State)
|
||||
case results(newItems: [C.Element], inCursor: C, after: State)
|
||||
case error(error: Error, after: State)
|
||||
case empty
|
||||
case exhausted
|
||||
|
||||
}
|
||||
|
||||
/// Enum represents possible load types for PaginationViewModel class
|
||||
///
|
||||
/// - reload: reload all items and reset cursor to initial state.
|
||||
/// - next: load next batch of items.
|
||||
public enum LoadType {
|
||||
|
||||
case reload
|
||||
case next
|
||||
|
||||
}
|
||||
|
||||
private var cursor: C
|
||||
|
||||
private let internalState = Variable<State>(.initial)
|
||||
|
||||
private var currentRequest: Disposable?
|
||||
|
||||
private let internalScheduler = SerialDispatchQueueScheduler(qos: .default)
|
||||
|
||||
/// Current PaginationViewModel state Driver
|
||||
public var state: Driver<State> {
|
||||
return internalState.asDriver()
|
||||
}
|
||||
|
||||
/// Initializer with enclosed cursor
|
||||
///
|
||||
/// - Parameter cursor: cursor to use for pagination
|
||||
public init(cursor: C) {
|
||||
self.cursor = cursor
|
||||
}
|
||||
|
||||
/// Mathod which triggers loading of items.
|
||||
///
|
||||
/// - Parameter loadType: type of loading. See LoadType enum.
|
||||
public func load(_ loadType: LoadType) {
|
||||
switch loadType {
|
||||
case .reload:
|
||||
currentRequest?.dispose()
|
||||
cursor = cursor.reset()
|
||||
|
||||
internalState.value = .loading(after: internalState.value)
|
||||
case .next:
|
||||
if case .exhausted(_) = internalState.value {
|
||||
fatalError("You shouldn't call load(.next) after got .exhausted state!")
|
||||
}
|
||||
|
||||
internalState.value = .loadingMore(after: internalState.value)
|
||||
}
|
||||
|
||||
let currentCursor = cursor
|
||||
|
||||
currentRequest = currentCursor.loadNextBatch()
|
||||
.subscribeOn(internalScheduler)
|
||||
.subscribe(onNext: { [weak self] newItems in
|
||||
self?.onGot(newItems: newItems, using: currentCursor)
|
||||
}, onError: { [weak self] error in
|
||||
self?.onGot(error: error)
|
||||
})
|
||||
}
|
||||
|
||||
private func onGot(newItems: [C.Element], using cursor: C) {
|
||||
internalState.value = .results(newItems: newItems, inCursor: cursor, after: internalState.value)
|
||||
|
||||
if cursor.exhausted {
|
||||
internalState.value = .exhausted
|
||||
}
|
||||
}
|
||||
|
||||
private func onGot(error: Error) {
|
||||
if case .exhausted? = error as? CursorError, case .loading(let after) = internalState.value {
|
||||
switch after {
|
||||
case .initial, .empty: // cursor exhausted after creation
|
||||
internalState.value = .empty
|
||||
default:
|
||||
internalState.value = .error(error: error, after: internalState.value)
|
||||
}
|
||||
} else {
|
||||
internalState.value = .error(error: error, after: internalState.value)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -27,7 +27,7 @@ open class XibView: UIView {
|
|||
|
||||
/// Nib name used to instantiate inner view
|
||||
open var innerViewNibName: String {
|
||||
return className(of: self)
|
||||
return type(of: self).xibName
|
||||
}
|
||||
|
||||
public convenience init() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,6 +70,9 @@ public extension CGImage {
|
|||
return nil
|
||||
}
|
||||
|
||||
ctx.translateBy(x: 0, y: size.height)
|
||||
ctx.scaleBy(x: 1.0, y: -1.0)
|
||||
|
||||
view.layer.render(in: ctx)
|
||||
|
||||
return ctx.makeImage()
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,86 @@
|
|||
//
|
||||
// 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 extension PaginationTableViewWrapperDelegate {
|
||||
|
||||
func emptyPlaceholder(forPaginationWrapper wrapper: PaginationTableViewWrapper<Cursor, Self>) -> UIView {
|
||||
let placeholder = UIView()
|
||||
|
||||
let label = UILabel()
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
label.text = "There is nothing here"
|
||||
|
||||
placeholder.addSubview(label)
|
||||
label.centerXAnchor.constraint(equalTo: placeholder.centerXAnchor).isActive = true
|
||||
label.centerYAnchor.constraint(equalTo: placeholder.centerYAnchor).isActive = true
|
||||
|
||||
return placeholder
|
||||
}
|
||||
|
||||
func errorPlaceholder(forPaginationWrapper wrapper: PaginationTableViewWrapper<Cursor, Self>,
|
||||
forError error: Error) -> UIView {
|
||||
|
||||
let placeholder = UIView()
|
||||
|
||||
let label = UILabel()
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
label.text = "An error has occurred"
|
||||
|
||||
placeholder.addSubview(label)
|
||||
label.centerXAnchor.constraint(equalTo: placeholder.centerXAnchor).isActive = true
|
||||
label.centerYAnchor.constraint(equalTo: placeholder.centerYAnchor).isActive = true
|
||||
|
||||
return placeholder
|
||||
}
|
||||
|
||||
func initialLoadingIndicator(forPaginationWrapper wrapper: PaginationTableViewWrapper<Cursor, Self>)
|
||||
-> AnyLoadingIndicator {
|
||||
|
||||
let indicator = UIActivityIndicatorView(activityIndicatorStyle: .whiteLarge)
|
||||
indicator.color = .gray
|
||||
|
||||
return AnyLoadingIndicator(indicator)
|
||||
}
|
||||
|
||||
func loadingMoreIndicator(forPaginationWrapper wrapper: PaginationTableViewWrapper<Cursor, Self>)
|
||||
-> AnyLoadingIndicator {
|
||||
|
||||
let indicator = UIActivityIndicatorView(activityIndicatorStyle: .gray)
|
||||
|
||||
return AnyLoadingIndicator(indicator)
|
||||
}
|
||||
|
||||
func retryLoadMoreButton(forPaginationWrapper wrapper: PaginationTableViewWrapper<Cursor, Self>) -> UIButton {
|
||||
let retryButton = UIButton(type: .custom)
|
||||
retryButton.backgroundColor = .lightGray
|
||||
retryButton.setTitle("Retry load more", for: .normal)
|
||||
|
||||
return retryButton
|
||||
}
|
||||
|
||||
func retryLoadMoreButtonHeight(forPaginationWrapper wrapper: PaginationTableViewWrapper<Cursor, Self>) -> CGFloat {
|
||||
return 44
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
//
|
||||
// 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 extension Support where Base: UIScrollView {
|
||||
|
||||
public var refreshControl: UIRefreshControl? {
|
||||
if #available(iOS 10.0, *) {
|
||||
return base.refreshControl
|
||||
} else {
|
||||
return base.subviews.first { $0 is UIRefreshControl } as? UIRefreshControl
|
||||
}
|
||||
}
|
||||
|
||||
public func setRefreshControl(_ newRefreshControl: UIRefreshControl?) {
|
||||
if #available(iOS 10.0, *) {
|
||||
base.refreshControl = newRefreshControl
|
||||
} else {
|
||||
if let newControl = newRefreshControl {
|
||||
refreshControl?.removeFromSuperview()
|
||||
base.addSubview(newControl)
|
||||
} else {
|
||||
refreshControl?.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
//
|
||||
// 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
|
||||
|
||||
extension UIActivityIndicatorView: Animatable {}
|
||||
|
||||
extension UIActivityIndicatorView: LoadingIndicator {}
|
||||
|
|
@ -34,9 +34,9 @@ public extension UICollectionView {
|
|||
/// - bundle: The bundle in which to search for the nib file.
|
||||
/// If you specify nil, this method looks for the nib file in the main bundle.
|
||||
public func registerNib<T>(forCellClass cellClass: T.Type, bundle: Bundle? = nil)
|
||||
where T: ReuseIdentifierProtocol, T: UICollectionViewCell, T: StaticNibNameProtocol {
|
||||
where T: ReuseIdentifierProtocol, T: UICollectionViewCell, T: XibNameProtocol {
|
||||
|
||||
register(UINib(nibName: T.nibName, bundle: bundle), forCellWithReuseIdentifier: T.reuseIdentifier)
|
||||
register(UINib(nibName: T.xibName, bundle: bundle), forCellWithReuseIdentifier: T.reuseIdentifier)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,14 +22,9 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
extension UIView: StaticNibNameProtocol {
|
||||
extension UIView: XibNameProtocol {
|
||||
|
||||
/**
|
||||
default implementation of StaticNibNameProtocol
|
||||
|
||||
- returns: class name string
|
||||
*/
|
||||
open class var nibName: String {
|
||||
open class var xibName: String {
|
||||
return className(of: self)
|
||||
}
|
||||
|
||||
|
|
@ -30,8 +30,8 @@ public extension UIView {
|
|||
/// - Parameter bundle: The bundle in which to search for the nib file.
|
||||
/// If you specify nil, this method looks for the nib file in the main bundle.
|
||||
/// - Returns: UIView or UIView subclass instance
|
||||
public static func loadFromNib<T>(bundle: Bundle? = nil) -> T where T: StaticNibNameProtocol, T: UIView {
|
||||
return loadFromNib(named: T.nibName, bundle: bundle)
|
||||
public static func loadFromNib<T>(bundle: Bundle? = nil) -> T where T: XibNameProtocol, T: UIView {
|
||||
return loadFromNib(named: T.xibName, bundle: bundle)
|
||||
}
|
||||
|
||||
/// Method which loads UIView (or subclass) instance from nib using given nib name parameter
|
||||
|
|
@ -46,7 +46,7 @@ public extension UIView {
|
|||
let nib = UINib(nibName: nibName, bundle: bundle)
|
||||
|
||||
guard let nibView = nib.instantiate(withOwner: owner, options: nil).first as? T else {
|
||||
fatalError("Can't nstantiate nib view with type \(T.self)")
|
||||
fatalError("Can't instantiate nib view with type \(T.self)")
|
||||
}
|
||||
|
||||
return nibView
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
//
|
||||
// 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
|
||||
|
||||
extension LoadingIndicator where Self: UIView {
|
||||
|
||||
public var view: Self {
|
||||
return self
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -20,9 +20,9 @@
|
|||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
extension UIView {
|
||||
public extension UIView {
|
||||
|
||||
private static let rotationKeyPath = "transform.rotation.z"
|
||||
|
||||
|
|
|
|||
|
|
@ -22,13 +22,8 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
extension UIViewController {
|
||||
extension UIViewController: XibNameProtocol {
|
||||
|
||||
/**
|
||||
Name of related xib
|
||||
|
||||
- returns: type name string
|
||||
*/
|
||||
open class var xibName: String {
|
||||
return typeName(of: self)
|
||||
}
|
||||
|
|
@ -63,5 +63,5 @@ public extension UIWindow {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]>
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
//
|
||||
// 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
|
||||
|
||||
/// Protocol that ensures that specific type support basic animation actions.
|
||||
public protocol Animatable {
|
||||
|
||||
/// Method that starts animation.
|
||||
func startAnimating()
|
||||
/// Method that stops animation.
|
||||
func stopAnimating()
|
||||
|
||||
}
|
||||
|
||||
/// Protocol that describes badic loading indicator.
|
||||
public protocol LoadingIndicator {
|
||||
|
||||
/// Type of view. Should be instance of UIView with basic animation actions.
|
||||
associatedtype View: UIView, Animatable
|
||||
|
||||
/// The underlying view.
|
||||
var view: View { get }
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
//
|
||||
// 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
|
||||
|
||||
/// Protocol that ensures that specific type can init new resetted instance from another instance.
|
||||
public protocol ResettableType {
|
||||
|
||||
/// Initializer with other instance parameter.
|
||||
///
|
||||
/// - Parameter other: Other instance of specific type.
|
||||
init(initialFrom other: Self)
|
||||
|
||||
}
|
||||
|
||||
public extension ResettableType {
|
||||
|
||||
/// Method that creates new resseted instance of self
|
||||
///
|
||||
/// - Returns: resseted instance of self
|
||||
func reset() -> Self {
|
||||
return Self(initialFrom: self)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
//
|
||||
// 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
|
||||
|
||||
/**
|
||||
Use `Support` proxy as customization point for constrained protocol extensions.
|
||||
|
||||
General pattern would be:
|
||||
|
||||
// 1. Extend Support protocol with constrain on Base
|
||||
// Read as: Support Extension where Base is a SomeType
|
||||
extension Support where Base: SomeType {
|
||||
// 2. Put any specific reactive extension for SomeType here
|
||||
}
|
||||
|
||||
With this approach we can have more specialized methods and properties using
|
||||
`Base` and not just specialized on common base type.
|
||||
|
||||
*/
|
||||
public struct Support<Base> {
|
||||
/// Base object to extend.
|
||||
public let base: Base
|
||||
|
||||
/// Creates extensions with base object.
|
||||
///
|
||||
/// - parameter base: Base object.
|
||||
public init(_ base: Base) {
|
||||
self.base = base
|
||||
}
|
||||
}
|
||||
|
||||
/// A type that has reactive extensions.
|
||||
public protocol SupportCompatible {
|
||||
/// Extended type
|
||||
associatedtype CompatibleType
|
||||
|
||||
/// Support extensions.
|
||||
static var support: Support<CompatibleType>.Type { get set }
|
||||
|
||||
/// Support extensions.
|
||||
var support: Support<CompatibleType> { get set }
|
||||
}
|
||||
|
||||
public extension SupportCompatible {
|
||||
/// Support extensions.
|
||||
public static var support: Support<Self>.Type {
|
||||
get {
|
||||
return Support<Self>.self
|
||||
}
|
||||
set {
|
||||
// this enables using Support to "mutate" base type
|
||||
}
|
||||
}
|
||||
|
||||
/// Support extensions.
|
||||
public var support: Support<Self> {
|
||||
get {
|
||||
return Support(self)
|
||||
}
|
||||
set {
|
||||
// this enables using Support to "mutate" base object
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension NSObject: SupportCompatible {}
|
||||
|
|
@ -22,12 +22,10 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
* protocol which ensures that specific type can return nib name of view
|
||||
*/
|
||||
public protocol StaticNibNameProtocol {
|
||||
/**
|
||||
- returns: nib name string
|
||||
*/
|
||||
static var nibName: String { get }
|
||||
/// Protocol that ensures that specific type can return it's xib name
|
||||
public protocol XibNameProtocol {
|
||||
|
||||
/// Name of related xib
|
||||
static var xibName: String { get }
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
//
|
||||
// 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
|
||||
|
||||
/// Type that performs some kind of type erasure for LoadingIndicator.
|
||||
public struct AnyLoadingIndicator: Animatable {
|
||||
|
||||
private let internalView: UIView
|
||||
private let animatableView: Animatable
|
||||
|
||||
/// Initializer with indicator that should be wrapped.
|
||||
///
|
||||
/// - Parameter _: indicator for wrapping.
|
||||
public init<Indicator>(_ base: Indicator) where Indicator: LoadingIndicator {
|
||||
self.internalView = base.view
|
||||
self.animatableView = base.view
|
||||
}
|
||||
|
||||
/// The indicator view.
|
||||
var view: UIView {
|
||||
return internalView
|
||||
}
|
||||
|
||||
public func startAnimating() {
|
||||
animatableView.startAnimating()
|
||||
}
|
||||
|
||||
public func stopAnimating() {
|
||||
animatableView.stopAnimating()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
import LeadKit
|
||||
|
||||
let str = "Hello, LeadKit playground"
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<playground version='5.0' target-platform='ios'>
|
||||
<timeline fileName='timeline.xctimeline'/>
|
||||
</playground>
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
//
|
||||
// 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 CursorTests: XCTestCase {
|
||||
|
||||
let disposeBag = DisposeBag()
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testStubCursor() {
|
||||
let entityCursor = StubCursor()
|
||||
|
||||
let cursorExpectation = expectation(description: "Stub cursor expectation")
|
||||
|
||||
entityCursor.loadNextBatch()
|
||||
.subscribe(onNext: { _ in
|
||||
cursorExpectation.fulfill()
|
||||
})
|
||||
.addDisposableTo(disposeBag)
|
||||
|
||||
waitForExpectations(timeout: 10, handler: nil)
|
||||
}
|
||||
|
||||
func testFixedPageCursor() {
|
||||
let stubCursor = StubCursor(maxItemsCount: 15, requestDelay: .milliseconds(100))
|
||||
let fixedPageCursor = FixedPageCursor(cursor: stubCursor, pageSize: 16)
|
||||
|
||||
let cursorExpectation = expectation(description: "Fixed page cursor expectation")
|
||||
let cursorExpectationError = expectation(description: "Fixed page cursor error expectation")
|
||||
|
||||
fixedPageCursor.loadNextBatch()
|
||||
.subscribe(onNext: { loadedItems in
|
||||
XCTAssertEqual(loadedItems.count, 15)
|
||||
|
||||
cursorExpectation.fulfill()
|
||||
})
|
||||
.addDisposableTo(disposeBag)
|
||||
|
||||
fixedPageCursor.loadNextBatch()
|
||||
.subscribe(onNext: { _ in
|
||||
XCTFail("Cursor should be exhausted!")
|
||||
}, onError: { error in
|
||||
switch try? cast(error) as CursorError {
|
||||
case .exhausted?:
|
||||
cursorExpectationError.fulfill()
|
||||
default:
|
||||
XCTFail("Cursor should be exhausted!")
|
||||
}
|
||||
})
|
||||
.addDisposableTo(disposeBag)
|
||||
|
||||
waitForExpectations(timeout: 10, handler: nil)
|
||||
}
|
||||
|
||||
func testMapCursorWithFixedPageCursor() {
|
||||
let stubCursor = StubCursor(maxItemsCount: 16, requestDelay: .milliseconds(100))
|
||||
let mapCursor = stubCursor.flatMap { $0.userId > 1 ? nil : $0 }
|
||||
let fixedPageCursor = FixedPageCursor(cursor: mapCursor, pageSize: 20)
|
||||
|
||||
let cursorExpectation = expectation(description: "Fixed page cursor expectation")
|
||||
|
||||
fixedPageCursor.loadNextBatch()
|
||||
.subscribe(onNext: { loadedItems in
|
||||
XCTAssertEqual(loadedItems.count, 8)
|
||||
|
||||
cursorExpectation.fulfill()
|
||||
})
|
||||
.addDisposableTo(disposeBag)
|
||||
|
||||
waitForExpectations(timeout: 10, handler: nil)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
//
|
||||
// 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: ResettableCursorType {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
required init(initialFrom other: StubCursor) {
|
||||
self.maxItemsCount = other.maxItemsCount
|
||||
self.requestDelay = other.requestDelay
|
||||
}
|
||||
|
||||
func loadNextBatch() -> Observable<[Post]> {
|
||||
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..<maxNewPosts])
|
||||
|
||||
observer.onNext(self[countBefore..<self.count])
|
||||
observer.onCompleted()
|
||||
})
|
||||
}
|
||||
|
||||
return Disposables.create()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -30,12 +30,7 @@ class MappableUserDefaultsTests: XCTestCase {
|
|||
return Post(userId: 1, postId: 1, title: "First post", body: "")
|
||||
}()
|
||||
|
||||
lazy var posts: [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: "")]
|
||||
}()
|
||||
let posts = Post.generate()
|
||||
|
||||
let userDefaults = UserDefaults.standard
|
||||
|
||||
|
|
|
|||
|
|
@ -62,3 +62,14 @@ extension Post: Equatable {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
extension Post {
|
||||
|
||||
static func generate() -> [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: "")]
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -11,7 +11,8 @@ target 'LeadKit' do
|
|||
pod "ObjectMapper", '~> 2.1'
|
||||
pod "Toast-Swift", '~> 2.0.0'
|
||||
pod "TableKit", '~> 2.3.1'
|
||||
|
||||
pod "UIScrollView-InfiniteScroll", '~> 1.0.0'
|
||||
|
||||
target 'LeadKitTests' do
|
||||
inherit! :search_paths
|
||||
# Pods for testing
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ PODS:
|
|||
- RxSwift (3.2.0)
|
||||
- TableKit (2.3.1)
|
||||
- Toast-Swift (2.0.0)
|
||||
- UIScrollView-InfiniteScroll (1.0.0)
|
||||
|
||||
DEPENDENCIES:
|
||||
- CocoaLumberjack/Swift (~> 3.1.0)
|
||||
|
|
@ -23,6 +24,7 @@ DEPENDENCIES:
|
|||
- RxSwift (= 3.2.0)
|
||||
- TableKit (~> 2.3.1)
|
||||
- Toast-Swift (~> 2.0.0)
|
||||
- UIScrollView-InfiniteScroll (~> 1.0.0)
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
Alamofire: dc44b1600b800eb63da6a19039a0083d62a6a62d
|
||||
|
|
@ -33,7 +35,8 @@ SPEC CHECKSUMS:
|
|||
RxSwift: 46574f70d416b7923c237195939cc488a7fbf3a0
|
||||
TableKit: 02e041b443f75fa3e9f1ee6024d4b256305bd904
|
||||
Toast-Swift: 5b2f8f720f7e78e48511f693df1f9c9a6e38a25a
|
||||
UIScrollView-InfiniteScroll: d26885be71caca7485cdb37eab513a8f89036bb0
|
||||
|
||||
PODFILE CHECKSUM: c85ee069c7ba0c53c44a486be3daf0ff5ed4e9fa
|
||||
PODFILE CHECKSUM: 0e4c2bc8339733ce0009cdae7684c9cdf03a9be0
|
||||
|
||||
COCOAPODS: 1.2.0
|
||||
|
|
|
|||
Loading…
Reference in New Issue