Merge pull request #58 from petropavel13/master

Table view pagination
This commit is contained in:
Nikolai Ashanin 2017-05-03 13:05:27 +03:00 committed by GitHub
commit 9b73354cc0
37 changed files with 1507 additions and 109 deletions

View File

@ -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

View File

@ -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;

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}
}

View File

@ -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() {

View File

@ -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
}

View File

@ -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()

View File

@ -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]
}
}

View File

@ -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
}
}

View File

@ -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()
}
}
}
}

View File

@ -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 {}

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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

View File

@ -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
}
}

View File

@ -20,9 +20,9 @@
// THE SOFTWARE.
//
import Foundation
import UIKit
extension UIView {
public extension UIView {
private static let rotationKeyPath = "transform.rotation.z"

View File

@ -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)
}

View File

@ -63,5 +63,5 @@ public extension UIWindow {
})
}
}
}

View File

@ -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]>
}

View File

@ -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 }
}

View File

@ -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)
}
}

View File

@ -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 {}

View File

@ -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 }
}

View File

@ -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()
}
}

View File

@ -0,0 +1,3 @@
import LeadKit
let str = "Hello, LeadKit playground"

View File

@ -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>

View File

@ -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)
}
}

View File

@ -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()
}
}
}

View File

@ -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

View File

@ -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: "")]
}
}

View File

@ -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)
}
}

View File

@ -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

View File

@ -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