├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── .travis.yml ├── Example ├── InfiniteLayout.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── InfiniteLayout-Example.xcscheme ├── InfiniteLayout │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── LaunchScreen.xib │ │ └── Main.storyboard │ ├── BaseCollectionViewController.swift │ ├── Cell.swift │ ├── CustomLayout.swift │ ├── CustomViewController.swift │ ├── Images.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Info.plist │ ├── PickerController.swift │ └── RxBaseCollectionViewController.swift ├── Podfile ├── Podfile.lock └── Tests │ ├── Info.plist │ └── Tests.swift ├── InfiniteLayout.podspec ├── LICENSE ├── Package.swift ├── README.md ├── Sources ├── CocoaProxy │ ├── CocoaProxy.h │ └── CocoaProxy.m ├── InfiniteLayout │ ├── InfiniteCollectionView.swift │ ├── InfiniteCollectionViewController.swift │ ├── InfiniteCollectionViewProxy.swift │ ├── InfiniteDataSource.swift │ ├── InfiniteLayout.swift │ └── SPMBridge.swift └── Rx │ ├── InfiniteCollectionView+Rx.swift │ ├── RxInfiniteCollectionView.swift │ └── RxInfiniteCollectionViewDataSource.swift ├── _Pods.xcodeproj ├── custom.gif ├── delegate.gif ├── horizontal.gif └── vertical.gif /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | 4 | # Xcode 5 | build/ 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata/ 15 | *.xccheckout 16 | profile 17 | *.moved-aside 18 | DerivedData 19 | *.hmap 20 | *.ipa 21 | 22 | # Bundler 23 | .bundle 24 | 25 | Carthage 26 | # We recommend against adding the Pods directory to your .gitignore. However 27 | # you should judge for yourself, the pros and cons are mentioned at: 28 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 29 | # 30 | # Note: if you ignore the Pods directory, make sure to uncomment 31 | # `pod install` in .travis.yml 32 | # 33 | # Pods/ 34 | /Example/InfiniteLayout.xcworkspace 35 | /Example/Pods 36 | /Example/InfiniteLayout.xcodeproj/project.xcworkspace/xcshareddata 37 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # references: 2 | # * http://www.objc.io/issue-6/travis-ci.html 3 | # * https://github.com/supermarin/xcpretty#usage 4 | 5 | osx_image: xcode10.2 6 | language: swift 7 | before_install: 8 | - gem install cocoapods # Since Travis is not always on latest version 9 | - pod repo update 10 | - pod install --project-directory=Example 11 | script: 12 | - set -o pipefail && xcodebuild -workspace Example/InfiniteLayout.xcworkspace -scheme InfiniteLayout-Example -sdk iphonesimulator build CODE_SIGNING_REQUIRED=NO | xcpretty -c 13 | - pod lib lint 14 | -------------------------------------------------------------------------------- /Example/InfiniteLayout.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 5259B95F4E58F763F6C70333 /* Pods_InfiniteLayout_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A6830D8B970E919B725A0D5E /* Pods_InfiniteLayout_Example.framework */; }; 11 | 607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD51AFB9204008FA782 /* AppDelegate.swift */; }; 12 | 607FACD81AFB9204008FA782 /* CustomViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD71AFB9204008FA782 /* CustomViewController.swift */; }; 13 | 607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 607FACD91AFB9204008FA782 /* Main.storyboard */; }; 14 | 607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDC1AFB9204008FA782 /* Images.xcassets */; }; 15 | 607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */; }; 16 | 607FACEC1AFB9204008FA782 /* Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACEB1AFB9204008FA782 /* Tests.swift */; }; 17 | D403973F1FEBFA9D006C41D2 /* Cell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D403973E1FEBFA9D006C41D2 /* Cell.swift */; }; 18 | D4162A391FF502A900AC2572 /* CustomLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4162A381FF502A900AC2572 /* CustomLayout.swift */; }; 19 | D43362D11FF64B330040C679 /* BaseCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D43362CF1FF64B330040C679 /* BaseCollectionViewController.swift */; }; 20 | D495544E1FFD3D920081225B /* RxBaseCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D495544D1FFD3D920081225B /* RxBaseCollectionViewController.swift */; }; 21 | D4B9816E1FFCCC900016C676 /* PickerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4B9816D1FFCCC900016C676 /* PickerController.swift */; }; 22 | E997A539C8278E7A661C9F91 /* Pods_InfiniteLayout_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C50F79B0DFFDE95D322480B2 /* Pods_InfiniteLayout_Tests.framework */; }; 23 | /* End PBXBuildFile section */ 24 | 25 | /* Begin PBXContainerItemProxy section */ 26 | 607FACE61AFB9204008FA782 /* PBXContainerItemProxy */ = { 27 | isa = PBXContainerItemProxy; 28 | containerPortal = 607FACC81AFB9204008FA782 /* Project object */; 29 | proxyType = 1; 30 | remoteGlobalIDString = 607FACCF1AFB9204008FA782; 31 | remoteInfo = InfiniteLayout; 32 | }; 33 | /* End PBXContainerItemProxy section */ 34 | 35 | /* Begin PBXFileReference section */ 36 | 17E9A86BF3432BF233E3D297 /* Pods-InfiniteLayout_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-InfiniteLayout_Example.debug.xcconfig"; path = "Pods/Target Support Files/Pods-InfiniteLayout_Example/Pods-InfiniteLayout_Example.debug.xcconfig"; sourceTree = ""; }; 37 | 2E68337E21F08944069DC023 /* Pods-InfiniteLayout_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-InfiniteLayout_Tests.release.xcconfig"; path = "Pods/Target Support Files/Pods-InfiniteLayout_Tests/Pods-InfiniteLayout_Tests.release.xcconfig"; sourceTree = ""; }; 38 | 5DA98D9A451E79A872E96E69 /* InfiniteLayout.podspec */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = InfiniteLayout.podspec; path = ../InfiniteLayout.podspec; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; 39 | 607FACD01AFB9204008FA782 /* InfiniteLayout_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = InfiniteLayout_Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 40 | 607FACD41AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 41 | 607FACD51AFB9204008FA782 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 42 | 607FACD71AFB9204008FA782 /* CustomViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomViewController.swift; sourceTree = ""; }; 43 | 607FACDA1AFB9204008FA782 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 44 | 607FACDC1AFB9204008FA782 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 45 | 607FACDF1AFB9204008FA782 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; 46 | 607FACE51AFB9204008FA782 /* InfiniteLayout_Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = InfiniteLayout_Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 47 | 607FACEA1AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 48 | 607FACEB1AFB9204008FA782 /* Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests.swift; sourceTree = ""; }; 49 | A6830D8B970E919B725A0D5E /* Pods_InfiniteLayout_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_InfiniteLayout_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 50 | AD3D900D0067605DEAFF148B /* Pods-InfiniteLayout_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-InfiniteLayout_Example.release.xcconfig"; path = "Pods/Target Support Files/Pods-InfiniteLayout_Example/Pods-InfiniteLayout_Example.release.xcconfig"; sourceTree = ""; }; 51 | C50F79B0DFFDE95D322480B2 /* Pods_InfiniteLayout_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_InfiniteLayout_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 52 | D403973E1FEBFA9D006C41D2 /* Cell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cell.swift; sourceTree = ""; }; 53 | D4162A381FF502A900AC2572 /* CustomLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomLayout.swift; sourceTree = ""; }; 54 | D43362CF1FF64B330040C679 /* BaseCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseCollectionViewController.swift; sourceTree = ""; }; 55 | D495544D1FFD3D920081225B /* RxBaseCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RxBaseCollectionViewController.swift; sourceTree = ""; }; 56 | D4B9816D1FFCCC900016C676 /* PickerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickerController.swift; sourceTree = ""; }; 57 | EF577FD009F0E2F96F1CECC6 /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = ""; }; 58 | F900DDE615FA1056EB58998B /* Pods-InfiniteLayout_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-InfiniteLayout_Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-InfiniteLayout_Tests/Pods-InfiniteLayout_Tests.debug.xcconfig"; sourceTree = ""; }; 59 | FD6AC573B93C313B5BDFF3FE /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; 60 | /* End PBXFileReference section */ 61 | 62 | /* Begin PBXFrameworksBuildPhase section */ 63 | 607FACCD1AFB9204008FA782 /* Frameworks */ = { 64 | isa = PBXFrameworksBuildPhase; 65 | buildActionMask = 2147483647; 66 | files = ( 67 | 5259B95F4E58F763F6C70333 /* Pods_InfiniteLayout_Example.framework in Frameworks */, 68 | ); 69 | runOnlyForDeploymentPostprocessing = 0; 70 | }; 71 | 607FACE21AFB9204008FA782 /* Frameworks */ = { 72 | isa = PBXFrameworksBuildPhase; 73 | buildActionMask = 2147483647; 74 | files = ( 75 | E997A539C8278E7A661C9F91 /* Pods_InfiniteLayout_Tests.framework in Frameworks */, 76 | ); 77 | runOnlyForDeploymentPostprocessing = 0; 78 | }; 79 | /* End PBXFrameworksBuildPhase section */ 80 | 81 | /* Begin PBXGroup section */ 82 | 607FACC71AFB9204008FA782 = { 83 | isa = PBXGroup; 84 | children = ( 85 | 607FACF51AFB993E008FA782 /* Podspec Metadata */, 86 | 607FACD21AFB9204008FA782 /* Example for InfiniteLayout */, 87 | 607FACE81AFB9204008FA782 /* Tests */, 88 | 607FACD11AFB9204008FA782 /* Products */, 89 | E938139B99B9D91C14707C07 /* Pods */, 90 | B8E153BE0A71F9235FE984C6 /* Frameworks */, 91 | ); 92 | sourceTree = ""; 93 | }; 94 | 607FACD11AFB9204008FA782 /* Products */ = { 95 | isa = PBXGroup; 96 | children = ( 97 | 607FACD01AFB9204008FA782 /* InfiniteLayout_Example.app */, 98 | 607FACE51AFB9204008FA782 /* InfiniteLayout_Tests.xctest */, 99 | ); 100 | name = Products; 101 | sourceTree = ""; 102 | }; 103 | 607FACD21AFB9204008FA782 /* Example for InfiniteLayout */ = { 104 | isa = PBXGroup; 105 | children = ( 106 | 607FACD51AFB9204008FA782 /* AppDelegate.swift */, 107 | D4B981781FFD1FEF0016C676 /* Swift */, 108 | D4B981791FFD1FF50016C676 /* Rx */, 109 | 607FACD91AFB9204008FA782 /* Main.storyboard */, 110 | 607FACDC1AFB9204008FA782 /* Images.xcassets */, 111 | 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */, 112 | 607FACD31AFB9204008FA782 /* Supporting Files */, 113 | ); 114 | name = "Example for InfiniteLayout"; 115 | path = InfiniteLayout; 116 | sourceTree = ""; 117 | }; 118 | 607FACD31AFB9204008FA782 /* Supporting Files */ = { 119 | isa = PBXGroup; 120 | children = ( 121 | 607FACD41AFB9204008FA782 /* Info.plist */, 122 | ); 123 | name = "Supporting Files"; 124 | sourceTree = ""; 125 | }; 126 | 607FACE81AFB9204008FA782 /* Tests */ = { 127 | isa = PBXGroup; 128 | children = ( 129 | 607FACEB1AFB9204008FA782 /* Tests.swift */, 130 | 607FACE91AFB9204008FA782 /* Supporting Files */, 131 | ); 132 | path = Tests; 133 | sourceTree = ""; 134 | }; 135 | 607FACE91AFB9204008FA782 /* Supporting Files */ = { 136 | isa = PBXGroup; 137 | children = ( 138 | 607FACEA1AFB9204008FA782 /* Info.plist */, 139 | ); 140 | name = "Supporting Files"; 141 | sourceTree = ""; 142 | }; 143 | 607FACF51AFB993E008FA782 /* Podspec Metadata */ = { 144 | isa = PBXGroup; 145 | children = ( 146 | 5DA98D9A451E79A872E96E69 /* InfiniteLayout.podspec */, 147 | FD6AC573B93C313B5BDFF3FE /* README.md */, 148 | EF577FD009F0E2F96F1CECC6 /* LICENSE */, 149 | ); 150 | name = "Podspec Metadata"; 151 | sourceTree = ""; 152 | }; 153 | B8E153BE0A71F9235FE984C6 /* Frameworks */ = { 154 | isa = PBXGroup; 155 | children = ( 156 | A6830D8B970E919B725A0D5E /* Pods_InfiniteLayout_Example.framework */, 157 | C50F79B0DFFDE95D322480B2 /* Pods_InfiniteLayout_Tests.framework */, 158 | ); 159 | name = Frameworks; 160 | sourceTree = ""; 161 | }; 162 | D43362CC1FF64AD30040C679 /* Custom */ = { 163 | isa = PBXGroup; 164 | children = ( 165 | 607FACD71AFB9204008FA782 /* CustomViewController.swift */, 166 | D4162A381FF502A900AC2572 /* CustomLayout.swift */, 167 | ); 168 | name = Custom; 169 | sourceTree = ""; 170 | }; 171 | D43362D31FF64B9E0040C679 /* Base */ = { 172 | isa = PBXGroup; 173 | children = ( 174 | D403973E1FEBFA9D006C41D2 /* Cell.swift */, 175 | D43362CF1FF64B330040C679 /* BaseCollectionViewController.swift */, 176 | ); 177 | name = Base; 178 | sourceTree = ""; 179 | }; 180 | D4B9816C1FFCCC5B0016C676 /* Picker */ = { 181 | isa = PBXGroup; 182 | children = ( 183 | D4B9816D1FFCCC900016C676 /* PickerController.swift */, 184 | ); 185 | name = Picker; 186 | sourceTree = ""; 187 | }; 188 | D4B981781FFD1FEF0016C676 /* Swift */ = { 189 | isa = PBXGroup; 190 | children = ( 191 | D43362D31FF64B9E0040C679 /* Base */, 192 | D43362CC1FF64AD30040C679 /* Custom */, 193 | D4B9816C1FFCCC5B0016C676 /* Picker */, 194 | ); 195 | name = Swift; 196 | sourceTree = ""; 197 | }; 198 | D4B981791FFD1FF50016C676 /* Rx */ = { 199 | isa = PBXGroup; 200 | children = ( 201 | D495544D1FFD3D920081225B /* RxBaseCollectionViewController.swift */, 202 | ); 203 | name = Rx; 204 | sourceTree = ""; 205 | }; 206 | E938139B99B9D91C14707C07 /* Pods */ = { 207 | isa = PBXGroup; 208 | children = ( 209 | 17E9A86BF3432BF233E3D297 /* Pods-InfiniteLayout_Example.debug.xcconfig */, 210 | AD3D900D0067605DEAFF148B /* Pods-InfiniteLayout_Example.release.xcconfig */, 211 | F900DDE615FA1056EB58998B /* Pods-InfiniteLayout_Tests.debug.xcconfig */, 212 | 2E68337E21F08944069DC023 /* Pods-InfiniteLayout_Tests.release.xcconfig */, 213 | ); 214 | name = Pods; 215 | sourceTree = ""; 216 | }; 217 | /* End PBXGroup section */ 218 | 219 | /* Begin PBXNativeTarget section */ 220 | 607FACCF1AFB9204008FA782 /* InfiniteLayout_Example */ = { 221 | isa = PBXNativeTarget; 222 | buildConfigurationList = 607FACEF1AFB9204008FA782 /* Build configuration list for PBXNativeTarget "InfiniteLayout_Example" */; 223 | buildPhases = ( 224 | DF297DCD753BC67E73166377 /* [CP] Check Pods Manifest.lock */, 225 | 607FACCC1AFB9204008FA782 /* Sources */, 226 | 607FACCD1AFB9204008FA782 /* Frameworks */, 227 | 607FACCE1AFB9204008FA782 /* Resources */, 228 | 36EEEA4F62F7A41DC7E00F9B /* [CP] Embed Pods Frameworks */, 229 | ); 230 | buildRules = ( 231 | ); 232 | dependencies = ( 233 | ); 234 | name = InfiniteLayout_Example; 235 | productName = InfiniteLayout; 236 | productReference = 607FACD01AFB9204008FA782 /* InfiniteLayout_Example.app */; 237 | productType = "com.apple.product-type.application"; 238 | }; 239 | 607FACE41AFB9204008FA782 /* InfiniteLayout_Tests */ = { 240 | isa = PBXNativeTarget; 241 | buildConfigurationList = 607FACF21AFB9204008FA782 /* Build configuration list for PBXNativeTarget "InfiniteLayout_Tests" */; 242 | buildPhases = ( 243 | BB5E5FB53E7C451064DBC9A2 /* [CP] Check Pods Manifest.lock */, 244 | 607FACE11AFB9204008FA782 /* Sources */, 245 | 607FACE21AFB9204008FA782 /* Frameworks */, 246 | 607FACE31AFB9204008FA782 /* Resources */, 247 | ); 248 | buildRules = ( 249 | ); 250 | dependencies = ( 251 | 607FACE71AFB9204008FA782 /* PBXTargetDependency */, 252 | ); 253 | name = InfiniteLayout_Tests; 254 | productName = Tests; 255 | productReference = 607FACE51AFB9204008FA782 /* InfiniteLayout_Tests.xctest */; 256 | productType = "com.apple.product-type.bundle.unit-test"; 257 | }; 258 | /* End PBXNativeTarget section */ 259 | 260 | /* Begin PBXProject section */ 261 | 607FACC81AFB9204008FA782 /* Project object */ = { 262 | isa = PBXProject; 263 | attributes = { 264 | LastSwiftUpdateCheck = 0830; 265 | LastUpgradeCheck = 1020; 266 | ORGANIZATIONNAME = CocoaPods; 267 | TargetAttributes = { 268 | 607FACCF1AFB9204008FA782 = { 269 | CreatedOnToolsVersion = 6.3.1; 270 | DevelopmentTeam = CPE39ZT2AY; 271 | LastSwiftMigration = 1020; 272 | }; 273 | 607FACE41AFB9204008FA782 = { 274 | CreatedOnToolsVersion = 6.3.1; 275 | DevelopmentTeam = CPE39ZT2AY; 276 | LastSwiftMigration = 1020; 277 | TestTargetID = 607FACCF1AFB9204008FA782; 278 | }; 279 | }; 280 | }; 281 | buildConfigurationList = 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "InfiniteLayout" */; 282 | compatibilityVersion = "Xcode 3.2"; 283 | developmentRegion = English; 284 | hasScannedForEncodings = 0; 285 | knownRegions = ( 286 | English, 287 | en, 288 | Base, 289 | ); 290 | mainGroup = 607FACC71AFB9204008FA782; 291 | productRefGroup = 607FACD11AFB9204008FA782 /* Products */; 292 | projectDirPath = ""; 293 | projectRoot = ""; 294 | targets = ( 295 | 607FACCF1AFB9204008FA782 /* InfiniteLayout_Example */, 296 | 607FACE41AFB9204008FA782 /* InfiniteLayout_Tests */, 297 | ); 298 | }; 299 | /* End PBXProject section */ 300 | 301 | /* Begin PBXResourcesBuildPhase section */ 302 | 607FACCE1AFB9204008FA782 /* Resources */ = { 303 | isa = PBXResourcesBuildPhase; 304 | buildActionMask = 2147483647; 305 | files = ( 306 | 607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */, 307 | 607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */, 308 | 607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */, 309 | ); 310 | runOnlyForDeploymentPostprocessing = 0; 311 | }; 312 | 607FACE31AFB9204008FA782 /* Resources */ = { 313 | isa = PBXResourcesBuildPhase; 314 | buildActionMask = 2147483647; 315 | files = ( 316 | ); 317 | runOnlyForDeploymentPostprocessing = 0; 318 | }; 319 | /* End PBXResourcesBuildPhase section */ 320 | 321 | /* Begin PBXShellScriptBuildPhase section */ 322 | 36EEEA4F62F7A41DC7E00F9B /* [CP] Embed Pods Frameworks */ = { 323 | isa = PBXShellScriptBuildPhase; 324 | buildActionMask = 2147483647; 325 | files = ( 326 | ); 327 | inputPaths = ( 328 | "${PODS_ROOT}/Target Support Files/Pods-InfiniteLayout_Example/Pods-InfiniteLayout_Example-frameworks.sh", 329 | "${BUILT_PRODUCTS_DIR}/Differentiator/Differentiator.framework", 330 | "${BUILT_PRODUCTS_DIR}/InfiniteLayout/InfiniteLayout.framework", 331 | "${BUILT_PRODUCTS_DIR}/RxCocoa/RxCocoa.framework", 332 | "${BUILT_PRODUCTS_DIR}/RxDataSources/RxDataSources.framework", 333 | "${BUILT_PRODUCTS_DIR}/RxRelay/RxRelay.framework", 334 | "${BUILT_PRODUCTS_DIR}/RxSwift/RxSwift.framework", 335 | ); 336 | name = "[CP] Embed Pods Frameworks"; 337 | outputPaths = ( 338 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Differentiator.framework", 339 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/InfiniteLayout.framework", 340 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxCocoa.framework", 341 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxDataSources.framework", 342 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxRelay.framework", 343 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxSwift.framework", 344 | ); 345 | runOnlyForDeploymentPostprocessing = 0; 346 | shellPath = /bin/sh; 347 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-InfiniteLayout_Example/Pods-InfiniteLayout_Example-frameworks.sh\"\n"; 348 | showEnvVarsInLog = 0; 349 | }; 350 | BB5E5FB53E7C451064DBC9A2 /* [CP] Check Pods Manifest.lock */ = { 351 | isa = PBXShellScriptBuildPhase; 352 | buildActionMask = 2147483647; 353 | files = ( 354 | ); 355 | inputPaths = ( 356 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 357 | "${PODS_ROOT}/Manifest.lock", 358 | ); 359 | name = "[CP] Check Pods Manifest.lock"; 360 | outputPaths = ( 361 | "$(DERIVED_FILE_DIR)/Pods-InfiniteLayout_Tests-checkManifestLockResult.txt", 362 | ); 363 | runOnlyForDeploymentPostprocessing = 0; 364 | shellPath = /bin/sh; 365 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 366 | showEnvVarsInLog = 0; 367 | }; 368 | DF297DCD753BC67E73166377 /* [CP] Check Pods Manifest.lock */ = { 369 | isa = PBXShellScriptBuildPhase; 370 | buildActionMask = 2147483647; 371 | files = ( 372 | ); 373 | inputPaths = ( 374 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 375 | "${PODS_ROOT}/Manifest.lock", 376 | ); 377 | name = "[CP] Check Pods Manifest.lock"; 378 | outputPaths = ( 379 | "$(DERIVED_FILE_DIR)/Pods-InfiniteLayout_Example-checkManifestLockResult.txt", 380 | ); 381 | runOnlyForDeploymentPostprocessing = 0; 382 | shellPath = /bin/sh; 383 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 384 | showEnvVarsInLog = 0; 385 | }; 386 | /* End PBXShellScriptBuildPhase section */ 387 | 388 | /* Begin PBXSourcesBuildPhase section */ 389 | 607FACCC1AFB9204008FA782 /* Sources */ = { 390 | isa = PBXSourcesBuildPhase; 391 | buildActionMask = 2147483647; 392 | files = ( 393 | 607FACD81AFB9204008FA782 /* CustomViewController.swift in Sources */, 394 | 607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */, 395 | D495544E1FFD3D920081225B /* RxBaseCollectionViewController.swift in Sources */, 396 | D4162A391FF502A900AC2572 /* CustomLayout.swift in Sources */, 397 | D4B9816E1FFCCC900016C676 /* PickerController.swift in Sources */, 398 | D403973F1FEBFA9D006C41D2 /* Cell.swift in Sources */, 399 | D43362D11FF64B330040C679 /* BaseCollectionViewController.swift in Sources */, 400 | ); 401 | runOnlyForDeploymentPostprocessing = 0; 402 | }; 403 | 607FACE11AFB9204008FA782 /* Sources */ = { 404 | isa = PBXSourcesBuildPhase; 405 | buildActionMask = 2147483647; 406 | files = ( 407 | 607FACEC1AFB9204008FA782 /* Tests.swift in Sources */, 408 | ); 409 | runOnlyForDeploymentPostprocessing = 0; 410 | }; 411 | /* End PBXSourcesBuildPhase section */ 412 | 413 | /* Begin PBXTargetDependency section */ 414 | 607FACE71AFB9204008FA782 /* PBXTargetDependency */ = { 415 | isa = PBXTargetDependency; 416 | target = 607FACCF1AFB9204008FA782 /* InfiniteLayout_Example */; 417 | targetProxy = 607FACE61AFB9204008FA782 /* PBXContainerItemProxy */; 418 | }; 419 | /* End PBXTargetDependency section */ 420 | 421 | /* Begin PBXVariantGroup section */ 422 | 607FACD91AFB9204008FA782 /* Main.storyboard */ = { 423 | isa = PBXVariantGroup; 424 | children = ( 425 | 607FACDA1AFB9204008FA782 /* Base */, 426 | ); 427 | name = Main.storyboard; 428 | sourceTree = ""; 429 | }; 430 | 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */ = { 431 | isa = PBXVariantGroup; 432 | children = ( 433 | 607FACDF1AFB9204008FA782 /* Base */, 434 | ); 435 | name = LaunchScreen.xib; 436 | sourceTree = ""; 437 | }; 438 | /* End PBXVariantGroup section */ 439 | 440 | /* Begin XCBuildConfiguration section */ 441 | 607FACED1AFB9204008FA782 /* Debug */ = { 442 | isa = XCBuildConfiguration; 443 | buildSettings = { 444 | ALWAYS_SEARCH_USER_PATHS = NO; 445 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 446 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 447 | CLANG_CXX_LIBRARY = "libc++"; 448 | CLANG_ENABLE_MODULES = YES; 449 | CLANG_ENABLE_OBJC_ARC = YES; 450 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 451 | CLANG_WARN_BOOL_CONVERSION = YES; 452 | CLANG_WARN_COMMA = YES; 453 | CLANG_WARN_CONSTANT_CONVERSION = YES; 454 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 455 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 456 | CLANG_WARN_EMPTY_BODY = YES; 457 | CLANG_WARN_ENUM_CONVERSION = YES; 458 | CLANG_WARN_INFINITE_RECURSION = YES; 459 | CLANG_WARN_INT_CONVERSION = YES; 460 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 461 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 462 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 463 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 464 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 465 | CLANG_WARN_STRICT_PROTOTYPES = YES; 466 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 467 | CLANG_WARN_UNREACHABLE_CODE = YES; 468 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 469 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 470 | COPY_PHASE_STRIP = NO; 471 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 472 | ENABLE_STRICT_OBJC_MSGSEND = YES; 473 | ENABLE_TESTABILITY = YES; 474 | GCC_C_LANGUAGE_STANDARD = gnu99; 475 | GCC_DYNAMIC_NO_PIC = NO; 476 | GCC_NO_COMMON_BLOCKS = YES; 477 | GCC_OPTIMIZATION_LEVEL = 0; 478 | GCC_PREPROCESSOR_DEFINITIONS = ( 479 | "DEBUG=1", 480 | "$(inherited)", 481 | ); 482 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 483 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 484 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 485 | GCC_WARN_UNDECLARED_SELECTOR = YES; 486 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 487 | GCC_WARN_UNUSED_FUNCTION = YES; 488 | GCC_WARN_UNUSED_VARIABLE = YES; 489 | IPHONEOS_DEPLOYMENT_TARGET = 9.3; 490 | MTL_ENABLE_DEBUG_INFO = YES; 491 | ONLY_ACTIVE_ARCH = YES; 492 | SDKROOT = iphoneos; 493 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 494 | SWIFT_VERSION = 5.0; 495 | }; 496 | name = Debug; 497 | }; 498 | 607FACEE1AFB9204008FA782 /* Release */ = { 499 | isa = XCBuildConfiguration; 500 | buildSettings = { 501 | ALWAYS_SEARCH_USER_PATHS = NO; 502 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 503 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 504 | CLANG_CXX_LIBRARY = "libc++"; 505 | CLANG_ENABLE_MODULES = YES; 506 | CLANG_ENABLE_OBJC_ARC = YES; 507 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 508 | CLANG_WARN_BOOL_CONVERSION = YES; 509 | CLANG_WARN_COMMA = YES; 510 | CLANG_WARN_CONSTANT_CONVERSION = YES; 511 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 512 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 513 | CLANG_WARN_EMPTY_BODY = YES; 514 | CLANG_WARN_ENUM_CONVERSION = YES; 515 | CLANG_WARN_INFINITE_RECURSION = YES; 516 | CLANG_WARN_INT_CONVERSION = YES; 517 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 518 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 519 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 520 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 521 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 522 | CLANG_WARN_STRICT_PROTOTYPES = YES; 523 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 524 | CLANG_WARN_UNREACHABLE_CODE = YES; 525 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 526 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 527 | COPY_PHASE_STRIP = NO; 528 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 529 | ENABLE_NS_ASSERTIONS = NO; 530 | ENABLE_STRICT_OBJC_MSGSEND = YES; 531 | GCC_C_LANGUAGE_STANDARD = gnu99; 532 | GCC_NO_COMMON_BLOCKS = YES; 533 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 534 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 535 | GCC_WARN_UNDECLARED_SELECTOR = YES; 536 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 537 | GCC_WARN_UNUSED_FUNCTION = YES; 538 | GCC_WARN_UNUSED_VARIABLE = YES; 539 | IPHONEOS_DEPLOYMENT_TARGET = 9.3; 540 | MTL_ENABLE_DEBUG_INFO = NO; 541 | SDKROOT = iphoneos; 542 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 543 | SWIFT_VERSION = 5.0; 544 | VALIDATE_PRODUCT = YES; 545 | }; 546 | name = Release; 547 | }; 548 | 607FACF01AFB9204008FA782 /* Debug */ = { 549 | isa = XCBuildConfiguration; 550 | baseConfigurationReference = 17E9A86BF3432BF233E3D297 /* Pods-InfiniteLayout_Example.debug.xcconfig */; 551 | buildSettings = { 552 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 553 | DEVELOPMENT_TEAM = CPE39ZT2AY; 554 | INFOPLIST_FILE = InfiniteLayout/Info.plist; 555 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 556 | MODULE_NAME = ExampleApp; 557 | PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)"; 558 | PRODUCT_NAME = "$(TARGET_NAME)"; 559 | SWIFT_VERSION = 5.0; 560 | TARGETED_DEVICE_FAMILY = 1; 561 | }; 562 | name = Debug; 563 | }; 564 | 607FACF11AFB9204008FA782 /* Release */ = { 565 | isa = XCBuildConfiguration; 566 | baseConfigurationReference = AD3D900D0067605DEAFF148B /* Pods-InfiniteLayout_Example.release.xcconfig */; 567 | buildSettings = { 568 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 569 | DEVELOPMENT_TEAM = CPE39ZT2AY; 570 | INFOPLIST_FILE = InfiniteLayout/Info.plist; 571 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 572 | MODULE_NAME = ExampleApp; 573 | PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)"; 574 | PRODUCT_NAME = "$(TARGET_NAME)"; 575 | SWIFT_VERSION = 5.0; 576 | TARGETED_DEVICE_FAMILY = 1; 577 | }; 578 | name = Release; 579 | }; 580 | 607FACF31AFB9204008FA782 /* Debug */ = { 581 | isa = XCBuildConfiguration; 582 | baseConfigurationReference = F900DDE615FA1056EB58998B /* Pods-InfiniteLayout_Tests.debug.xcconfig */; 583 | buildSettings = { 584 | DEVELOPMENT_TEAM = CPE39ZT2AY; 585 | FRAMEWORK_SEARCH_PATHS = ( 586 | "$(SDKROOT)/Developer/Library/Frameworks", 587 | "$(inherited)", 588 | ); 589 | GCC_PREPROCESSOR_DEFINITIONS = ( 590 | "DEBUG=1", 591 | "$(inherited)", 592 | ); 593 | INFOPLIST_FILE = Tests/Info.plist; 594 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 595 | PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)"; 596 | PRODUCT_NAME = "$(TARGET_NAME)"; 597 | SWIFT_VERSION = 5.0; 598 | }; 599 | name = Debug; 600 | }; 601 | 607FACF41AFB9204008FA782 /* Release */ = { 602 | isa = XCBuildConfiguration; 603 | baseConfigurationReference = 2E68337E21F08944069DC023 /* Pods-InfiniteLayout_Tests.release.xcconfig */; 604 | buildSettings = { 605 | DEVELOPMENT_TEAM = CPE39ZT2AY; 606 | FRAMEWORK_SEARCH_PATHS = ( 607 | "$(SDKROOT)/Developer/Library/Frameworks", 608 | "$(inherited)", 609 | ); 610 | INFOPLIST_FILE = Tests/Info.plist; 611 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 612 | PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)"; 613 | PRODUCT_NAME = "$(TARGET_NAME)"; 614 | SWIFT_VERSION = 5.0; 615 | }; 616 | name = Release; 617 | }; 618 | /* End XCBuildConfiguration section */ 619 | 620 | /* Begin XCConfigurationList section */ 621 | 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "InfiniteLayout" */ = { 622 | isa = XCConfigurationList; 623 | buildConfigurations = ( 624 | 607FACED1AFB9204008FA782 /* Debug */, 625 | 607FACEE1AFB9204008FA782 /* Release */, 626 | ); 627 | defaultConfigurationIsVisible = 0; 628 | defaultConfigurationName = Release; 629 | }; 630 | 607FACEF1AFB9204008FA782 /* Build configuration list for PBXNativeTarget "InfiniteLayout_Example" */ = { 631 | isa = XCConfigurationList; 632 | buildConfigurations = ( 633 | 607FACF01AFB9204008FA782 /* Debug */, 634 | 607FACF11AFB9204008FA782 /* Release */, 635 | ); 636 | defaultConfigurationIsVisible = 0; 637 | defaultConfigurationName = Release; 638 | }; 639 | 607FACF21AFB9204008FA782 /* Build configuration list for PBXNativeTarget "InfiniteLayout_Tests" */ = { 640 | isa = XCConfigurationList; 641 | buildConfigurations = ( 642 | 607FACF31AFB9204008FA782 /* Debug */, 643 | 607FACF41AFB9204008FA782 /* Release */, 644 | ); 645 | defaultConfigurationIsVisible = 0; 646 | defaultConfigurationName = Release; 647 | }; 648 | /* End XCConfigurationList section */ 649 | }; 650 | rootObject = 607FACC81AFB9204008FA782 /* Project object */; 651 | } 652 | -------------------------------------------------------------------------------- /Example/InfiniteLayout.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/InfiniteLayout.xcodeproj/xcshareddata/xcschemes/InfiniteLayout-Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 47 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 65 | 66 | 67 | 68 | 78 | 80 | 86 | 87 | 88 | 89 | 90 | 91 | 97 | 99 | 105 | 106 | 107 | 108 | 110 | 111 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /Example/InfiniteLayout/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // InfiniteLayout 4 | // 5 | // Created by Arnoymous on 12/20/2017. 6 | // Copyright (c) 2017 Arnoymous. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /Example/InfiniteLayout/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 25 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /Example/InfiniteLayout/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | -------------------------------------------------------------------------------- /Example/InfiniteLayout/BaseCollectionViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseCollectionViewController.swift 3 | // InfiniteLayout_Example 4 | // 5 | // Created by Arnaud Dorgans on 29/12/2017. 6 | // Copyright © 2017 CocoaPods. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import InfiniteLayout 11 | 12 | class BaseCollectionViewController: InfiniteCollectionViewController { 13 | 14 | override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 15 | guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as? Cell else { 16 | fatalError() 17 | } 18 | cell.update(index: self.infiniteCollectionView!.indexPath(from: indexPath).row) 19 | return cell 20 | } 21 | 22 | override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 23 | return 20 24 | } 25 | 26 | override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 27 | collectionView.scrollToItem(at: indexPath, at: self.infiniteCollectionView!.infiniteLayout.scrollDirection == .vertical ? .centeredVertically : .centeredHorizontally, animated: true) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Example/InfiniteLayout/Cell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Cell.swift 3 | // InfiniteLayout_Example 4 | // 5 | // Created by Arnaud Dorgans on 21/12/2017. 6 | // Copyright © 2017 CocoaPods. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class Cell: UICollectionViewCell { 12 | 13 | @IBOutlet weak var cellView: CellView! 14 | 15 | func update(index: Int) { 16 | cellView.update(index: index) 17 | } 18 | } 19 | 20 | enum CellStyle { 21 | case circular 22 | case `default` 23 | 24 | static let all = [circular, `default`] 25 | } 26 | 27 | @IBDesignable class CellView: UIView { 28 | 29 | let titleLabel = UILabel() 30 | 31 | let colors = [#colorLiteral(red: 0.5254901961, green: 0.6901960784, blue: 0.9137254902, alpha: 1), #colorLiteral(red: 0.5254901961, green: 0.6196078431, blue: 0.9137254902, alpha: 1), #colorLiteral(red: 0.6078431373, green: 0.5254901961, blue: 0.9137254902, alpha: 1), #colorLiteral(red: 0.9137254902, green: 0.5254901961, blue: 0.8392156863, alpha: 1), #colorLiteral(red: 0.9137254902, green: 0.5254901961, blue: 0.6, alpha: 1), #colorLiteral(red: 0.9137254902, green: 0.6784313725, blue: 0.5254901961, alpha: 1), #colorLiteral(red: 0.9137254902, green: 0.9058823529, blue: 0.5254901961, alpha: 1), #colorLiteral(red: 0.5254901961, green: 0.9137254902, blue: 0.5921568627, alpha: 1), #colorLiteral(red: 0.5254901961, green: 0.8, blue: 0.9137254902, alpha: 1)] 32 | 33 | @IBInspectable var styleIndex: Int { 34 | get { return CellStyle.all.firstIndex(of: style)! } 35 | set { style = CellStyle.all[newValue % CellStyle.all.count] } 36 | } 37 | 38 | var style: CellStyle = .default { 39 | didSet { 40 | updateStyle() 41 | } 42 | } 43 | 44 | override init(frame: CGRect) { 45 | super.init(frame: frame) 46 | sharedInit() 47 | } 48 | 49 | required init?(coder aDecoder: NSCoder) { 50 | super.init(coder: aDecoder) 51 | sharedInit() 52 | 53 | } 54 | 55 | override func awakeFromNib() { 56 | super.awakeFromNib() 57 | } 58 | 59 | func sharedInit() { 60 | titleLabel.translatesAutoresizingMaskIntoConstraints = false 61 | titleLabel.textColor = .white 62 | titleLabel.font = UIFont.boldSystemFont(ofSize: 32) 63 | self.addSubview(titleLabel) 64 | titleLabel.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true 65 | titleLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true 66 | } 67 | 68 | func update(index: Int) { 69 | self.titleLabel.text = String(index + 1) 70 | self.backgroundColor = colors[index % colors.count] 71 | } 72 | 73 | func updateStyle() { 74 | switch style { 75 | case .default: 76 | self.layer.cornerRadius = 8 77 | case .circular: 78 | self.layer.cornerRadius = self.frame.height / 2 79 | } 80 | } 81 | 82 | override func layoutSubviews() { 83 | super.layoutSubviews() 84 | updateStyle() 85 | } 86 | 87 | override func prepareForInterfaceBuilder() { 88 | self.update(index: 0) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Example/InfiniteLayout/CustomLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomLayout.swift 3 | // InfiniteLayout_Example 4 | // 5 | // Created by Arnaud Dorgans on 28/12/2017. 6 | // Copyright © 2017 CocoaPods. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import InfiniteLayout 11 | 12 | class CustomLayout: InfiniteLayout { 13 | 14 | let minimumScale: CGFloat = 0.75 15 | let rangeRatio: CGFloat = 1 16 | 17 | override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { 18 | let attributes = super.layoutAttributesForElements(in: rect).flatMap { 19 | self.copyLayoutAttributes(from: $0) 20 | } 21 | guard let visibleRect = self.visibleCollectionViewRect() else { 22 | return attributes 23 | } 24 | let centeredOffset = CGPoint(x: visibleRect.midX, y: visibleRect.midY) 25 | for attributes in attributes ?? [] { 26 | let diff = self.scrollDirection == .horizontal ? centeredOffset.x - attributes.center.x : centeredOffset.y - attributes.center.y 27 | let scale = max(min(diff / (min(visibleRect.width, visibleRect.height) * rangeRatio), 1), -1) 28 | attributes.transform = attributes.transform.translatedBy(x: abs(scale * (visibleRect.width / 2)), y: 0) 29 | attributes.transform = attributes.transform.rotated(by: scale * (CGFloat.pi / 2)) 30 | } 31 | return attributes 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Example/InfiniteLayout/CustomViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomViewController.swift 3 | // InfiniteLayout 4 | // 5 | // Created by Arnoymous on 12/20/2017. 6 | // Copyright (c) 2017 Arnoymous. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import InfiniteLayout 11 | 12 | class CustomViewController: UIViewController { 13 | 14 | @IBOutlet weak var infiniteCollectionView: InfiniteCollectionView! 15 | 16 | } 17 | 18 | extension CustomViewController: UICollectionViewDataSource { 19 | 20 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 21 | return 20 22 | } 23 | 24 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 25 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! Cell 26 | cell.update(index: self.infiniteCollectionView!.indexPath(from: indexPath).row) 27 | return cell 28 | } 29 | } 30 | 31 | extension CustomViewController: UICollectionViewDelegate { 32 | 33 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 34 | collectionView.scrollToItem(at: indexPath, at: .centeredVertically, animated: true) 35 | } 36 | } 37 | 38 | extension CustomViewController: UICollectionViewDelegateFlowLayout { 39 | 40 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 41 | return CGSize(width: 100, height: 100) 42 | } 43 | 44 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { 45 | return collectionView.frame.height 46 | } 47 | 48 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { 49 | return 10 50 | } 51 | 52 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { 53 | return UIEdgeInsets(top: 0, left: 0, bottom: 10, right: 0) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Example/InfiniteLayout/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ios-marketing", 45 | "size" : "1024x1024", 46 | "scale" : "1x" 47 | } 48 | ], 49 | "info" : { 50 | "version" : 1, 51 | "author" : "xcode" 52 | } 53 | } -------------------------------------------------------------------------------- /Example/InfiniteLayout/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/InfiniteLayout/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /Example/InfiniteLayout/PickerController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PickerController.swift 3 | // InfiniteLayout_Example 4 | // 5 | // Created by Arnaud Dorgans on 03/01/2018. 6 | // Copyright © 2018 CocoaPods. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import InfiniteLayout 11 | 12 | class PickerController: UIViewController { 13 | 14 | @IBOutlet weak var selectedView: CellView! 15 | @IBOutlet weak var infiniteCollectionView: InfiniteCollectionView! 16 | } 17 | 18 | extension PickerController: UICollectionViewDataSource { 19 | 20 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 21 | return 20 22 | } 23 | 24 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 25 | guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as? Cell else { 26 | fatalError() 27 | } 28 | cell.update(index: self.infiniteCollectionView.indexPath(from: indexPath).row) 29 | return cell 30 | } 31 | } 32 | 33 | extension PickerController: UICollectionViewDelegate { 34 | 35 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 36 | collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true) 37 | } 38 | } 39 | 40 | extension PickerController: InfiniteCollectionViewDelegate { 41 | 42 | func infiniteCollectionView(_ infiniteCollectionView: InfiniteCollectionView, didChangeCenteredIndexPath centeredIndexPath: IndexPath?) { 43 | guard let indexPath = centeredIndexPath else { 44 | return 45 | } 46 | self.selectedView.update(index: self.infiniteCollectionView.indexPath(from: indexPath).row) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Example/InfiniteLayout/RxBaseCollectionViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RxBaseCollectionViewController.swift 3 | // InfiniteLayout_Example 4 | // 5 | // Created by Arnaud Dorgans on 03/01/2018. 6 | // Copyright © 2018 CocoaPods. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | import RxDataSources 13 | import InfiniteLayout 14 | 15 | class RxBaseCollectionViewController: UIViewController { 16 | 17 | @IBOutlet weak var infiniteCollectionView: RxInfiniteCollectionView! 18 | 19 | let disposeBag = DisposeBag() 20 | 21 | override func viewDidLoad() { 22 | super.viewDidLoad() 23 | 24 | Observable.just(Array(0..<20)) 25 | .bind(to: infiniteCollectionView.rx.items(cellIdentifier: "cell", cellType: Cell.self, infinite: true)) { _, index, cell in 26 | cell.update(index: index) 27 | }.disposed(by: disposeBag) 28 | 29 | infiniteCollectionView.rx.modelCentered(Int.self) 30 | .asDriver() 31 | .drive(onNext: { current in 32 | print("centered: \(current + 1)") 33 | }).disposed(by: disposeBag) 34 | 35 | infiniteCollectionView.rx.itemSelected 36 | .asDriver() 37 | .drive(onNext: { [unowned self] indexPath in 38 | self.infiniteCollectionView.scrollToItem(at: indexPath, at: .centeredVertically, animated: true) 39 | }).disposed(by: disposeBag) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Example/Podfile: -------------------------------------------------------------------------------- 1 | use_frameworks! 2 | platform :ios, '10.0' 3 | 4 | target 'InfiniteLayout_Example' do 5 | pod 'InfiniteLayout', :path => '../' 6 | pod 'InfiniteLayout/Rx', :path => '../' 7 | 8 | target 'InfiniteLayout_Tests' do 9 | inherit! :search_paths 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /Example/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Differentiator (5.0.0) 3 | - InfiniteLayout (0.5): 4 | - InfiniteLayout/Core (= 0.5) 5 | - InfiniteLayout/CocoaProxy (0.5) 6 | - InfiniteLayout/Core (0.5): 7 | - InfiniteLayout/CocoaProxy 8 | - InfiniteLayout/Rx (0.5): 9 | - InfiniteLayout/Core 10 | - RxCocoa (~> 6) 11 | - RxDataSources (~> 5) 12 | - RxSwift (~> 6) 13 | - RxCocoa (6.2.0): 14 | - RxRelay (= 6.2.0) 15 | - RxSwift (= 6.2.0) 16 | - RxDataSources (5.0.0): 17 | - Differentiator (~> 5.0) 18 | - RxCocoa (~> 6.0) 19 | - RxSwift (~> 6.0) 20 | - RxRelay (6.2.0): 21 | - RxSwift (= 6.2.0) 22 | - RxSwift (6.2.0) 23 | 24 | DEPENDENCIES: 25 | - InfiniteLayout (from `../`) 26 | - InfiniteLayout/Rx (from `../`) 27 | 28 | SPEC REPOS: 29 | trunk: 30 | - Differentiator 31 | - RxCocoa 32 | - RxDataSources 33 | - RxRelay 34 | - RxSwift 35 | 36 | EXTERNAL SOURCES: 37 | InfiniteLayout: 38 | :path: "../" 39 | 40 | SPEC CHECKSUMS: 41 | Differentiator: e8497ceab83c1b10ca233716d547b9af21b9344d 42 | InfiniteLayout: 2c55b0fc14b6a9d924fbe13c0e2d88b1cbf5423d 43 | RxCocoa: 4baf94bb35f2c0ab31bc0cb9f1900155f646ba42 44 | RxDataSources: aa47cc1ed6c500fa0dfecac5c979b723542d79cf 45 | RxRelay: e72dbfd157807478401ef1982e1c61c945c94b2f 46 | RxSwift: d356ab7bee873611322f134c5f9ef379fa183d8f 47 | 48 | PODFILE CHECKSUM: 0dc7a9b37f9b7c5d5266beaab9e94861b80aa80f 49 | 50 | COCOAPODS: 1.10.1 51 | -------------------------------------------------------------------------------- /Example/Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /Example/Tests/Tests.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import XCTest 3 | import InfiniteLayout 4 | 5 | class Tests: XCTestCase { 6 | 7 | override func setUp() { 8 | super.setUp() 9 | // Put setup code here. This method is called before the invocation of each test method in the class. 10 | } 11 | 12 | override func tearDown() { 13 | // Put teardown code here. This method is called after the invocation of each test method in the class. 14 | super.tearDown() 15 | } 16 | 17 | func testExample() { 18 | // This is an example of a functional test case. 19 | XCTAssert(true, "Pass") 20 | } 21 | 22 | func testPerformanceExample() { 23 | // This is an example of a performance test case. 24 | self.measure() { 25 | // Put the code you want to measure the time of here. 26 | } 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /InfiniteLayout.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod lib lint InfiniteLayout.podspec' to ensure this is a 3 | # valid spec before submitting. 4 | # 5 | # Any lines starting with a # are optional, but their use is encouraged 6 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html 7 | # 8 | 9 | Pod::Spec.new do |s| 10 | s.name = 'InfiniteLayout' 11 | s.version = '0.5' 12 | s.summary = 'Horizontal and Vertical infinite scrolling feature for UICollectionView with Paging, NSProxy delegate, Reactive extension' 13 | 14 | # This description is used to generate tags and improve search results. 15 | # * Think: What does it do? Why did you write it? What is the focus? 16 | # * Try to keep it short, snappy and to the point. 17 | # * Write the description between the DESC delimiters below. 18 | # * Finally, don't worry about the indent, CocoaPods strips it! 19 | 20 | s.description = <<-DESC 21 | Horizontal and Vertical infinite scrolling feature for UICollectionView with Paging, NSProxy delegate, Reactive extension, SectionModel & AnimatableSectionModel support 22 | DESC 23 | 24 | s.homepage = 'https://github.com/arnauddorgans/InfiniteLayout' 25 | s.screenshots = 'https://github.com/arnauddorgans/InfiniteLayout/raw/master/horizontal.gif', 'https://github.com/arnauddorgans/InfiniteLayout/raw/master/vertical.gif', 'https://github.com/arnauddorgans/InfiniteLayout/raw/master/custom.gif', 'https://github.com/arnauddorgans/InfiniteLayout/raw/master/delegate.gif' 26 | s.license = { :type => 'MIT', :file => 'LICENSE' } 27 | s.author = { 'Arnaud Dorgans' => 'ineox@me.com' } 28 | s.source = { :git => 'https://github.com/arnauddorgans/InfiniteLayout.git', :tag => s.version.to_s } 29 | s.social_media_url = 'https://twitter.com/arnauddorgans' 30 | 31 | s.ios.deployment_target = '9.0' 32 | s.tvos.deployment_target = '9.0' 33 | 34 | #s.xcconfig = { 'SWIFT_OBJC_BRIDGING_HEADER' => '${POD_ROOT}/InfiniteLayout/BridgeHeader.h' } 35 | 36 | # s.resource_bundles = { 37 | # 'InfiniteLayout' => ['InfiniteLayout/Assets/*.png'] 38 | # } 39 | 40 | # s.public_header_files = 'Pod/Classes/**/*.h' 41 | # s.frameworks = 'UIKit', 'MapKit' 42 | s.swift_versions = ['5.0', '5.1'] 43 | 44 | 45 | s.default_subspec = 'Core' 46 | s.subspec 'Core' do |core| 47 | core.source_files = 'Sources/InfiniteLayout/**/*' 48 | core.dependency 'InfiniteLayout/CocoaProxy' 49 | core.exclude_files = '**/*/SPMBridge.swift' 50 | end 51 | s.subspec 'CocoaProxy' do |core| 52 | core.source_files = 'Sources/CocoaProxy/**/*' 53 | end 54 | s.subspec 'Rx' do |rx| 55 | rx.dependency 'InfiniteLayout/Core' 56 | rx.dependency 'RxSwift', '~> 6' 57 | rx.dependency 'RxCocoa', '~> 6' 58 | rx.dependency 'RxDataSources', '~> 5' 59 | rx.source_files = 'Sources/Rx/**/*' 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Arnoymous 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "InfiniteLayout", 8 | platforms: [.iOS(.v9), .tvOS(.v9)], 9 | products: [ 10 | .library( 11 | name: "InfiniteLayout", 12 | targets: ["InfiniteLayout"]), 13 | .library( 14 | name: "RxInfiniteLayout", 15 | targets: ["RxInfiniteLayout"]), 16 | ], 17 | dependencies: [ 18 | .package(url: "https://github.com/ReactiveX/RxSwift.git", .upToNextMajor(from: "6.0.0")), 19 | .package(url: "https://github.com/RxSwiftCommunity/RxDataSources.git", .upToNextMajor(from: "5.0.0")), 20 | ], 21 | targets: [ 22 | .target( 23 | name: "CocoaProxy", 24 | dependencies: [], 25 | publicHeadersPath: "./" 26 | ), 27 | .target( 28 | name: "InfiniteLayout", 29 | dependencies: ["CocoaProxy"] 30 | ), 31 | .target( 32 | name: "RxInfiniteLayout", 33 | dependencies: ["InfiniteLayout", "RxSwift", "RxCocoa", "RxDataSources"], 34 | path: "Sources/Rx" 35 | ), 36 | ] 37 | ) 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # InfiniteLayout 2 | 3 | [![CI Status](http://img.shields.io/travis/arnauddorgans/InfiniteLayout.svg?style=flat)](https://travis-ci.org/arnauddorgans/InfiniteLayout) 4 | [![License](https://img.shields.io/cocoapods/l/InfiniteLayout.svg?style=flat)](http://cocoapods.org/pods/InfiniteLayout) 5 | [![Platform](https://img.shields.io/cocoapods/p/InfiniteLayout.svg?style=flat)](http://cocoapods.org/pods/InfiniteLayout) 6 | [![Version](https://img.shields.io/cocoapods/v/InfiniteLayout.svg?style=flat)](http://cocoapods.org/pods/InfiniteLayout) 7 | [![Swift Package Manager compatible](https://img.shields.io/badge/Swift%20Package%20Manager-compatible-brightgreen.svg)](https://github.com/apple/swift-package-manager) 8 | 9 | 10 | 11 | 12 | ## Example 13 | 14 | To run the example project, clone the repo, and run `pod install` from the Example directory first. 15 | 16 | ## Installation 17 | 18 | ### [CocoaPods](https://guides.cocoapods.org/using/using-cocoapods.html) 19 | 20 | InfiniteLayout is available through [CocoaPods](http://cocoapods.org). To install 21 | it, simply add the following line to your Podfile: 22 | 23 | ```ruby 24 | pod 'InfiniteLayout' 25 | ``` 26 | 27 | 28 | ### [Swift Package Manager](https://github.com/apple/swift-package-manager) 29 | 30 | Create a `Package.swift` file. 31 | 32 | ```swift 33 | // swift-tools-version:5.0 34 | 35 | import PackageDescription 36 | 37 | let package = Package( 38 | name: "InfiniteLayoutTestProject", 39 | dependencies: [ 40 | .package(url: "https://github.com/arnauddorgans/InfiniteLayout.git", from: "0.4.2") 41 | ], 42 | targets: [ 43 | .target(name: "InfiniteLayoutTestProject", dependencies: ["InfiniteLayout"]) 44 | ] 45 | ) 46 | ``` 47 | 48 | ## Usage 49 | 50 | ```swift 51 | @IBOutlet weak var collectionView: InfiniteCollectionView! 52 | ``` 53 | 54 | InfiniteCollectionView doesn't need any other delegate or dataSource, 55 | just use UICollectionViewDataSource and UICollectionViewDelegate in the same way as you'll use it in any other UICollectionView. 56 | 57 | InfiniteLayout provides 3 classes for infinite scrolling: 58 | 59 | **InfiniteLayout**: an UICollectionViewFlowLayout 60 | 61 | **InfiniteCollectionView**: an UICollectionView with InfiniteLayout 62 | 63 | **InfiniteCollectionViewController**: an UICollectionViewController with InfiniteCollectionView 64 | 65 | ### IndexPath 66 | 67 | InfiniteCollectionView may create fake indexPath, 68 | 69 | To get the real indexPath call 70 | 71 | ```swift 72 | func indexPath(from infiniteIndexPath: IndexPath) -> IndexPath 73 | ``` 74 | 75 | To get the real section call 76 | 77 | ```swift 78 | func section(from infiniteSection: Int) -> Int 79 | ``` 80 | 81 | ### Paging 82 | 83 | InfiniteCollectionView provide a paging functionality, you can enable it by setting the **isItemPagingEnabled** flag to **true** 84 | 85 | ```swift 86 | self.infiniteCollectionView.isItemPagingEnabled = true 87 | ``` 88 | 89 | When the **isItemPagingEnabled** flag is enabled you can adjust the deceleration rate by setting the **velocityMultiplier**, the more the value is high, the more the deceleration is long 90 | 91 | ```swift 92 | self.infiniteCollectionView.velocityMultiplier = 1 // like scrollView with paging (default value) 93 | self.infiniteCollectionView.velocityMultiplier = 500 // like scrollView without paging 94 | ``` 95 | 96 | When the **isItemPagingEnabled** flag is enabled you can set a **preferredCenteredIndexPath**, this value is used to calculate the preferred visible cell to center each time the collectionView will change its contentSize 97 | 98 | ```swift 99 | self.infiniteCollectionView.preferredCenteredIndexPath = [0, 0] // center the cell at [0, 0] if visible (default value) 100 | self.infiniteCollectionView.preferredCenteredIndexPath = nil // center the closest cell from center 101 | ``` 102 | 103 | ### Delegate 104 | 105 | 106 | 107 | InfiniteCollectionView provide an **infiniteDelegate** protocol used to get the centered IndexPath, usefull if you want to use an InfiniteCollectionView like a Picker. 108 | 109 | ```swift 110 | func infiniteCollectionView(_ infiniteCollectionView: InfiniteCollectionView, didChangeCenteredIndexPath from: IndexPath?, to: IndexPath?) 111 | ``` 112 | 113 | ### Rx 114 | 115 | InfiniteCollectionView provide a subspec **InfiniteLayout/Rx** 116 | ```ruby 117 | pod 'InfiniteLayout/Rx' 118 | ``` 119 | 120 | To use InfiniteCollectionView with RxSwift without conflicts between NSProxy 121 | 122 | Use **RxInfiniteCollectionView** instead of **InfiniteCollectionView** 123 | 124 | ```swift 125 | @IBOutlet weak var collectionView: RxInfiniteCollectionView! 126 | ``` 127 | 128 | RxInfiniteCollectionView provides 2 dataSources for SectionModel: 129 | 130 | **RxInfiniteCollectionViewSectionedReloadDataSource** and **RxInfiniteCollectionViewSectionedAnimatedDataSource** 131 | 132 | #### Binding: 133 | 134 | Without sections: 135 | ```swift 136 | // automatic cell dequeue 137 | Observable.just(Array(0..<2)) 138 | .bind(to: infiniteCollectionView.rx.items(cellIdentifier: "cell", cellType: Cell.self, infinite: true)) { row, element, cell in 139 | cell.update(index: row) // update your cell 140 | }.disposed(by: disposeBag) 141 | 142 | // custom cell dequeue 143 | Observable.just(Array(0..<2)) 144 | .bind(to: infiniteCollectionView.rx.items(infinite: true)) { collectionView, row, element in 145 | let indexPath = IndexPath(row: row, section: 0) 146 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! Cell // dequeue your cell 147 | cell.update(index: row) // update your cell 148 | return cell 149 | }.disposed(by: disposeBag) 150 | ``` 151 | 152 | With sections: 153 | ```swift 154 | let dataSource = RxInfiniteCollectionViewSectionedReloadDataSource>(configureCell: { dataSource, collectionView, indexPath, element in 155 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! Cell // dequeue your cell 156 | cell.update(index: indexPath.row) // update your cell 157 | return cell 158 | }) 159 | 160 | Observable.just([ 161 | SectionModel(model: 0, items: Array(0..<2)), 162 | SectionModel(model: 1, items: Array(0..<10)) 163 | ]) 164 | .bind(to: infiniteCollectionView.rx.items(dataSource: dataSource)) 165 | .disposed(by: disposeBag) 166 | ``` 167 | 168 | for animations just use **RxInfiniteCollectionViewSectionedAnimatedDataSource** & **AnimatableSectionModel** 169 | 170 | #### Centered IndexPath: 171 | 172 | RxInfiniteCollectionView provide Reactive extension for **itemCentered** & **modelCentered** 173 | ```swift 174 | infiniteCollectionView.rx.itemCentered 175 | .asDriver() 176 | .drive(onNext: { [unowned self] indexPath in 177 | self.selectedView.update(index: indexPath.row) // update interface with indexPath 178 | }).disposed(by: disposeBag) 179 | 180 | infiniteCollectionView.rx.modelCentered(Int.self) 181 | .asDriver() 182 | .drive(onNext: { [unowned self] element in 183 | self.selectedView.update(index: element) // update interface with model 184 | }).disposed(by: disposeBag) 185 | ``` 186 | 187 | ## Author 188 | 189 | Arnaud Dorgans, arnaud.dorgans@gmail.com 190 | 191 | ## License 192 | 193 | InfiniteLayout is available under the MIT license. See the LICENSE file for more info. 194 | -------------------------------------------------------------------------------- /Sources/CocoaProxy/CocoaProxy.h: -------------------------------------------------------------------------------- 1 | // 2 | // CocoaProxy.h 3 | // CocoaProxy 4 | // 5 | // Created by Arnaud Dorgans on 27/12/2017. 6 | // 7 | 8 | #import 9 | 10 | @interface CocoaProxy : NSProxy 11 | 12 | - (instancetype _Nonnull)init; 13 | - (instancetype _Nonnull)initWithProxies:(nonnull NSArray>*)proxies; 14 | 15 | - (NSArray> *_Nonnull)proxiesForSelector:(SEL _Nonnull )aSelector; 16 | 17 | @property (nonatomic, strong) NSArray>* _Nonnull proxies; 18 | @property (nonatomic, copy) BOOL (^ _Nullable proxyFilter)(id _Nonnull proxy, SEL _Nonnull selector); 19 | 20 | @end 21 | -------------------------------------------------------------------------------- /Sources/CocoaProxy/CocoaProxy.m: -------------------------------------------------------------------------------- 1 | // 2 | // CocoaProxy.m 3 | // CocoaProxy 4 | // 5 | // Created by Arnaud Dorgans on 27/12/2017. 6 | // 7 | 8 | #import "CocoaProxy.h" 9 | 10 | @interface CocoaProxy () { } 11 | 12 | @property (nonatomic, strong) NSPointerArray *pointerArray; 13 | 14 | @end 15 | 16 | @implementation CocoaProxy 17 | 18 | - (instancetype _Nonnull)initWithProxies:(nonnull NSArray*)proxies { 19 | [self setProxies: proxies]; 20 | return self; 21 | } 22 | 23 | - (instancetype _Nonnull)init { 24 | return [self initWithProxies: @[]]; 25 | } 26 | 27 | - (BOOL)respondsToSelector:(SEL)aSelector 28 | { 29 | return ([self methodSignatureForSelector: aSelector] != nil); 30 | } 31 | 32 | - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector 33 | { 34 | for (NSObject* proxy in [self proxiesForSelector: aSelector]) { 35 | if ([proxy respondsToSelector: aSelector]) { 36 | return [proxy methodSignatureForSelector: aSelector]; 37 | } 38 | } 39 | return nil; 40 | } 41 | 42 | - (NSArray> *_Nonnull)proxiesForSelector:(SEL _Nonnull )aSelector 43 | { 44 | if (self.proxyFilter) { 45 | NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject, NSDictionary * _Nullable bindings) { 46 | return self.proxyFilter(evaluatedObject, aSelector); 47 | }]; 48 | return [self.proxies filteredArrayUsingPredicate: predicate]; 49 | } 50 | return self.proxies; 51 | } 52 | 53 | - (void)forwardInvocation:(NSInvocation *)invocation 54 | { 55 | for (NSObject* proxy in [self proxiesForSelector: invocation.selector]) { 56 | [self invokeInvocation: invocation onProxy: proxy]; 57 | } 58 | } 59 | 60 | - (BOOL)invokeInvocation:(NSInvocation *)invocation onProxy:(id)proxy 61 | { 62 | if ([proxy respondsToSelector: invocation.selector]) { 63 | [invocation invokeWithTarget: proxy]; 64 | return YES; 65 | } 66 | return NO; 67 | } 68 | 69 | - (void)setProxies:(NSArray> *)proxies { 70 | self.pointerArray = [NSPointerArray weakObjectsPointerArray]; 71 | for (NSObject* proxy in proxies) { 72 | [self.pointerArray addPointer: (void *)proxy]; 73 | } 74 | } 75 | 76 | - (NSArray> *)proxies { 77 | return self.pointerArray.allObjects; 78 | } 79 | 80 | @end 81 | -------------------------------------------------------------------------------- /Sources/InfiniteLayout/InfiniteCollectionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InfiniteCollectionView.swift 3 | // InfiniteLayout 4 | // 5 | // Created by Arnaud Dorgans on 20/12/2017. 6 | // 7 | 8 | import UIKit 9 | 10 | @objc public protocol InfiniteCollectionViewDelegate { 11 | 12 | @objc optional func infiniteCollectionView(_ infiniteCollectionView: InfiniteCollectionView, didChangeCenteredIndexPath from: IndexPath?, to: IndexPath?) 13 | } 14 | 15 | open class InfiniteCollectionView: UICollectionView { 16 | 17 | lazy var dataSourceProxy = InfiniteCollectionViewDataSourceProxy(collectionView: self) 18 | lazy var delegateProxy = InfiniteCollectionViewDelegateProxy(collectionView: self) 19 | 20 | @IBOutlet open weak var infiniteDelegate: InfiniteCollectionViewDelegate? 21 | 22 | open private(set) var centeredIndexPath: IndexPath? 23 | open var preferredCenteredIndexPath: IndexPath? = IndexPath(item: 0, section: 0) 24 | 25 | open var forwardDelegate: Bool { return true } 26 | var _contentSize: CGSize? 27 | 28 | override open weak var delegate: UICollectionViewDelegate? { 29 | get { return super.delegate } 30 | set { 31 | guard forwardDelegate else { 32 | super.delegate = newValue 33 | return 34 | } 35 | guard let newValue = newValue else { 36 | super.delegate = nil 37 | return 38 | } 39 | let isProxy = newValue is InfiniteCollectionViewDelegateProxy 40 | let delegate = isProxy ? newValue : delegateProxy 41 | if !isProxy { 42 | delegateProxy.delegate = newValue 43 | } 44 | super.delegate = delegate 45 | } 46 | } 47 | 48 | override open weak var dataSource: UICollectionViewDataSource? { 49 | get { return super.dataSource } 50 | set { 51 | guard forwardDelegate else { 52 | super.dataSource = newValue 53 | return 54 | } 55 | guard let newValue = newValue else { 56 | super.dataSource = nil 57 | return 58 | } 59 | let isProxy = newValue is InfiniteCollectionViewDataSourceProxy 60 | let dataSource = isProxy ? newValue : dataSourceProxy 61 | if !isProxy { 62 | dataSourceProxy.delegate = newValue 63 | } 64 | super.dataSource = dataSource 65 | } 66 | } 67 | 68 | @IBInspectable open var isItemPagingEnabled: Bool = false 69 | @IBInspectable open var velocityMultiplier: CGFloat = 1 { 70 | didSet { 71 | self.infiniteLayout.velocityMultiplier = velocityMultiplier 72 | } 73 | } 74 | 75 | public var infiniteLayout: InfiniteLayout! { 76 | return self.collectionViewLayout as? InfiniteLayout 77 | } 78 | 79 | private static func infiniteLayout(layout: UICollectionViewLayout) -> InfiniteLayout { 80 | guard let infiniteLayout = layout as? InfiniteLayout else { 81 | return InfiniteLayout(layout: layout) 82 | } 83 | return infiniteLayout 84 | } 85 | 86 | public override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) { 87 | super.init(frame: frame, collectionViewLayout: InfiniteCollectionView.infiniteLayout(layout: layout)) 88 | sharedInit() 89 | } 90 | 91 | required public init?(coder aDecoder: NSCoder) { 92 | super.init(coder: aDecoder) 93 | 94 | let infiniteLayout = InfiniteCollectionView.infiniteLayout(layout: self.collectionViewLayout) 95 | if self.collectionViewLayout != infiniteLayout { 96 | self.collectionViewLayout = infiniteLayout 97 | } 98 | } 99 | 100 | open override func awakeFromNib() { 101 | super.awakeFromNib() 102 | sharedInit() 103 | } 104 | 105 | private func sharedInit() { 106 | self.showsVerticalScrollIndicator = false 107 | self.showsHorizontalScrollIndicator = false 108 | #if os(iOS) 109 | self.scrollsToTop = false 110 | #endif 111 | } 112 | 113 | open override func layoutSubviews() { 114 | super.layoutSubviews() 115 | self.updateLayoutIfNeeded() 116 | } 117 | } 118 | 119 | // MARK: DataSource 120 | extension InfiniteCollectionView: UICollectionViewDataSource { 121 | 122 | private var delegateNumberOfSections: Int { 123 | guard let sections = dataSourceProxy.delegate.flatMap({ $0.numberOfSections?(in: self) ?? 1 }) else { 124 | fatalError("collectionView dataSource is required") 125 | } 126 | return sections 127 | } 128 | 129 | private func delegateNumberOfItems(in section: Int) -> Int { 130 | guard let items = dataSourceProxy.delegate.flatMap({ $0.collectionView(self, numberOfItemsInSection: self.section(from: section)) }) else { 131 | fatalError("collectionView dataSource is required") 132 | } 133 | return items 134 | } 135 | 136 | private var multiplier: Int { 137 | return InfiniteDataSources.multiplier(estimatedItemSize: self.infiniteLayout.itemSize, enabled: self.infiniteLayout.isEnabled) 138 | } 139 | 140 | public func section(from infiniteSection: Int) -> Int { 141 | return InfiniteDataSources.section(from: infiniteSection, numberOfSections: delegateNumberOfSections) 142 | } 143 | 144 | public func indexPath(from infiniteIndexPath: IndexPath) -> IndexPath { 145 | return InfiniteDataSources.indexPath(from: infiniteIndexPath, 146 | numberOfSections: delegateNumberOfSections, 147 | numberOfItems: delegateNumberOfItems(in: infiniteIndexPath.section)) 148 | } 149 | 150 | public func numberOfSections(in collectionView: UICollectionView) -> Int { 151 | return InfiniteDataSources.numberOfSections(numberOfSections: delegateNumberOfSections, multiplier: multiplier) 152 | } 153 | 154 | public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 155 | return InfiniteDataSources.numberOfItemsInSection(numberOfItemsInSection: delegateNumberOfItems(in: section), 156 | numberOfSections: delegateNumberOfSections, 157 | multiplier: multiplier) 158 | } 159 | 160 | public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 161 | fatalError("collectionView dataSource is required") 162 | } 163 | } 164 | 165 | extension InfiniteCollectionView: UICollectionViewDelegate { 166 | 167 | func updateLayoutIfNeeded() { 168 | self.loopCollectionViewIfNeeded() 169 | self.centerCollectionViewIfNeeded() 170 | 171 | let preferredVisibleIndexPath = infiniteLayout.preferredVisibleLayoutAttributes()?.indexPath 172 | if self.centeredIndexPath != preferredVisibleIndexPath { 173 | let previousCenteredIndexPath = self.centeredIndexPath 174 | self.centeredIndexPath = preferredVisibleIndexPath 175 | self.infiniteDelegate?.infiniteCollectionView?(self, didChangeCenteredIndexPath: previousCenteredIndexPath, to: self.centeredIndexPath) 176 | } 177 | } 178 | 179 | // MARK: Loop 180 | func loopCollectionViewIfNeeded() { 181 | self.infiniteLayout.loopCollectionViewIfNeeded() 182 | } 183 | 184 | public func scrollViewDidScroll(_ scrollView: UIScrollView) { 185 | delegateProxy.delegate?.scrollViewDidScroll?(scrollView) 186 | self.updateLayoutIfNeeded() 187 | } 188 | 189 | // MARK: Paging 190 | func centerCollectionViewIfNeeded() { 191 | guard isItemPagingEnabled, 192 | !self.isDragging && !self.isDecelerating else { 193 | return 194 | } 195 | guard self._contentSize != self.contentSize else { 196 | return 197 | } 198 | self._contentSize = self.contentSize 199 | self.infiniteLayout.centerCollectionViewIfNeeded(indexPath: self.preferredCenteredIndexPath) 200 | } 201 | 202 | public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { 203 | if isItemPagingEnabled { 204 | self.infiniteLayout.centerCollectionView(withVelocity: velocity, targetContentOffset: targetContentOffset) 205 | } 206 | self.delegateProxy.delegate?.scrollViewWillEndDragging?(scrollView, withVelocity: velocity, targetContentOffset: targetContentOffset) 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /Sources/InfiniteLayout/InfiniteCollectionViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InfiniteCollectionViewController.swift 3 | // InfiniteLayout 4 | // 5 | // Created by Arnaud Dorgans on 23/12/2017. 6 | // 7 | 8 | import UIKit 9 | 10 | open class InfiniteCollectionViewController: UICollectionViewController { 11 | 12 | public var infiniteCollectionView: InfiniteCollectionView? { 13 | return self.collectionView as? InfiniteCollectionView 14 | } 15 | 16 | override open func viewDidLoad() { 17 | super.viewDidLoad() 18 | 19 | guard let _ = self.infiniteCollectionView else { 20 | fatalError("InfiniteCollectionView is needed") 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/InfiniteLayout/InfiniteCollectionViewProxy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Proxy.swift 3 | // InfiniteLayout 4 | // 5 | // Created by Arnaud Dorgans on 20/12/2017. 6 | // 7 | 8 | import UIKit 9 | 10 | class InfiniteCollectionViewProxy: CocoaProxy { 11 | 12 | var collectionView: InfiniteCollectionView! { 13 | get { return self.proxies.first as? InfiniteCollectionView } 14 | set { 15 | if !self.proxies.isEmpty { 16 | self.proxies.removeFirst() 17 | } 18 | self.proxies.insert(newValue, at: 0) 19 | } 20 | } 21 | 22 | var delegate: T? { 23 | get { 24 | guard self.proxies.count > 1 else { 25 | return nil 26 | } 27 | return self.proxies.last as? T 28 | } set { 29 | while self.proxies.count > 1 { 30 | self.proxies.removeLast() 31 | } 32 | guard let delegate = newValue else { 33 | return 34 | } 35 | self.proxies.append(delegate) 36 | } 37 | } 38 | 39 | override func proxies(for aSelector: Selector) -> [NSObjectProtocol] { 40 | return super.proxies(for: aSelector).reversed() 41 | } 42 | 43 | init(collectionView: InfiniteCollectionView) { 44 | super.init(proxies: []) 45 | self.collectionView = collectionView 46 | } 47 | 48 | deinit { 49 | self.proxies.removeAll() 50 | } 51 | } 52 | 53 | class InfiniteCollectionViewDelegateProxy: InfiniteCollectionViewProxy, UICollectionViewDelegate { 54 | 55 | override func proxies(for aSelector: Selector) -> [NSObjectProtocol] { 56 | return super.proxies(for: aSelector) 57 | .first { proxy in 58 | guard !(aSelector == #selector(UIScrollViewDelegate.scrollViewDidScroll(_:)) || 59 | aSelector == #selector(UIScrollViewDelegate.scrollViewWillEndDragging(_:withVelocity:targetContentOffset:))) else { 60 | return proxy is InfiniteCollectionView 61 | } 62 | return true 63 | }.flatMap { [$0] } ?? [] 64 | } 65 | } 66 | 67 | class InfiniteCollectionViewDataSourceProxy: InfiniteCollectionViewProxy, UICollectionViewDataSource { 68 | 69 | override func proxies(for aSelector: Selector) -> [NSObjectProtocol] { 70 | return super.proxies(for: aSelector) 71 | .first { proxy in 72 | guard !(aSelector == #selector(UICollectionViewDataSource.numberOfSections(in:)) || 73 | aSelector == #selector(UICollectionViewDataSource.collectionView(_:numberOfItemsInSection:))) else { 74 | return proxy is InfiniteCollectionView 75 | } 76 | return true 77 | }.flatMap { [$0] } ?? [] 78 | } 79 | 80 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 81 | return self.collectionView.collectionView(collectionView, numberOfItemsInSection: section) 82 | } 83 | 84 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 85 | return self.delegate?.collectionView(collectionView, cellForItemAt: indexPath) ?? 86 | self.collectionView.collectionView(collectionView, cellForItemAt: indexPath) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Sources/InfiniteLayout/InfiniteDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InfiniteDataSources.swift 3 | // InfiniteLayout 4 | // 5 | // Created by Arnaud Dorgans on 03/01/2018. 6 | // 7 | 8 | import UIKit 9 | 10 | open class InfiniteDataSources { 11 | public static var originCount: Int = 0 12 | public static func section(from infiniteSection: Int, numberOfSections: Int) -> Int { 13 | return infiniteSection % numberOfSections 14 | } 15 | 16 | public static func indexPath(from infiniteIndexPath: IndexPath, numberOfSections: Int, numberOfItems: Int) -> IndexPath { 17 | return IndexPath(item: infiniteIndexPath.item % numberOfItems, section: self.section(from: infiniteIndexPath.section, numberOfSections: numberOfSections)) 18 | } 19 | 20 | public static func multiplier(estimatedItemSize: CGSize, enabled: Bool) -> Int { 21 | guard enabled else { 22 | return 1 23 | } 24 | let min = Swift.min(estimatedItemSize.width, estimatedItemSize.height) 25 | let count = ceil(InfiniteLayout.minimumContentSize / min) 26 | return Int(count) 27 | } 28 | 29 | public static func numberOfSections(numberOfSections: Int, multiplier: Int) -> Int { 30 | return numberOfSections > 1 ? numberOfSections * multiplier : numberOfSections 31 | } 32 | 33 | public static func numberOfItemsInSection(numberOfItemsInSection: Int, numberOfSections: Int, multiplier: Int) -> Int { 34 | originCount = numberOfItemsInSection 35 | return numberOfSections > 1 ? numberOfItemsInSection : numberOfItemsInSection * multiplier 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/InfiniteLayout/InfiniteLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InfiniteCollectionView.swift 3 | // InfiniteLayout 4 | // 5 | // Created by Arnaud Dorgans on 20/12/2017. 6 | // 7 | 8 | import UIKit 9 | 10 | open class InfiniteLayout: UICollectionViewFlowLayout { 11 | 12 | public var velocityMultiplier: CGFloat = 1 // used to simulate paging 13 | 14 | private let multiplier: CGFloat = 500 // contentOffset multiplier 15 | 16 | private var contentSize: CGSize = .zero 17 | 18 | private var hasValidLayout: Bool = false 19 | 20 | @IBInspectable public var isEnabled: Bool = true { 21 | didSet { 22 | self.invalidateLayout() 23 | } 24 | } 25 | 26 | public var currentPage: CGPoint { 27 | guard let collectionView = self.collectionView else { 28 | return .zero 29 | } 30 | return self.page(for: collectionView.contentOffset) 31 | } 32 | 33 | open override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { 34 | return true 35 | } 36 | 37 | public convenience init(layout: UICollectionViewLayout) { 38 | self.init() 39 | guard let layout = layout as? UICollectionViewFlowLayout else { 40 | return 41 | } 42 | self.scrollDirection = layout.scrollDirection 43 | self.minimumLineSpacing = layout.minimumLineSpacing 44 | self.minimumInteritemSpacing = layout.minimumInteritemSpacing 45 | self.itemSize = layout.itemSize 46 | self.sectionInset = layout.sectionInset 47 | self.headerReferenceSize = layout.headerReferenceSize 48 | self.footerReferenceSize = layout.footerReferenceSize 49 | } 50 | 51 | static var minimumContentSize: CGFloat { 52 | return max(UIScreen.main.bounds.width, UIScreen.main.bounds.height) * 4 53 | } 54 | 55 | override open func prepare() { 56 | let collectionViewContentSize = super.collectionViewContentSize 57 | self.contentSize = CGSize(width: collectionViewContentSize.width, height: collectionViewContentSize.height) 58 | self.hasValidLayout = { 59 | guard let collectionView = self.collectionView, collectionView.bounds != .zero, self.isEnabled else { 60 | return false 61 | } 62 | return (scrollDirection == .horizontal ? self.contentSize.width : self.contentSize.height) >= 63 | InfiniteLayout.minimumContentSize 64 | }() 65 | super.prepare() 66 | } 67 | 68 | override open var collectionViewContentSize: CGSize { 69 | guard hasValidLayout else { 70 | return self.contentSize 71 | } 72 | return CGSize(width: scrollDirection == .horizontal ? self.contentSize.width * multiplier : self.contentSize.width, 73 | height: scrollDirection == .vertical ? self.contentSize.height * multiplier : self.contentSize.height) 74 | } 75 | 76 | open override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { 77 | guard let attributes = super.layoutAttributesForItem(at: indexPath) else { 78 | return nil 79 | } 80 | return self.layoutAttributes(from: attributes, page: currentPage) 81 | } 82 | 83 | override open func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { 84 | guard hasValidLayout else { 85 | return super.layoutAttributesForElements(in: rect) 86 | } 87 | let page = self.page(for: rect.origin) 88 | var elements = [UICollectionViewLayoutAttributes]() 89 | var rect = self.rect(from: rect) 90 | if (self.scrollDirection == .horizontal && rect.maxX > contentSize.width) || 91 | (self.scrollDirection == .vertical && rect.maxY > contentSize.height) { 92 | let diffRect = CGRect(origin: .zero, size: CGSize(width: self.scrollDirection == .horizontal ? rect.maxX - contentSize.width : rect.width, 93 | height: self.scrollDirection == .vertical ? rect.maxY - contentSize.height : rect.height)) 94 | elements.append(contentsOf: self.elements(in: diffRect, page: self.page(from: page, offset: 1))) 95 | if self.scrollDirection == .horizontal { 96 | rect.size.width -= diffRect.width 97 | } else { 98 | rect.size.height -= diffRect.height 99 | } 100 | } 101 | elements.append(contentsOf: self.elements(in: rect, page: page)) 102 | return elements 103 | } 104 | 105 | private func page(for point: CGPoint) -> CGPoint { 106 | let xPage: CGFloat = floor(point.x / contentSize.width) 107 | let yPage: CGFloat = floor(point.y / contentSize.height) 108 | 109 | return CGPoint(x: self.scrollDirection == .horizontal ? xPage : 0, 110 | y: self.scrollDirection == .vertical ? yPage : 0) 111 | } 112 | 113 | private func page(from page: CGPoint, offset: CGFloat) -> CGPoint { 114 | return CGPoint(x: self.scrollDirection == .horizontal ? page.x + offset : page.x, 115 | y: self.scrollDirection == .vertical ? page.y + offset : page.y) 116 | } 117 | 118 | private func pageIndex(from page: CGPoint) -> CGFloat { 119 | return self.scrollDirection == .horizontal ? page.x : page.y 120 | } 121 | 122 | public func rect(from rect: CGRect, page: CGPoint = .zero) -> CGRect { 123 | var rect = rect 124 | if self.scrollDirection == .horizontal && rect.origin.x < 0 { 125 | rect.origin.x += abs(floor(contentSize.width / rect.origin.x)) * contentSize.width 126 | } else if self.scrollDirection == .vertical && rect.origin.y < 0 { 127 | rect.origin.y += abs(floor(contentSize.height / rect.origin.y)) * contentSize.height 128 | } 129 | rect.origin.x = rect.origin.x.truncatingRemainder(dividingBy: contentSize.width) 130 | rect.origin.y = rect.origin.y.truncatingRemainder(dividingBy: contentSize.height) 131 | rect.origin.x += page.x * contentSize.width 132 | rect.origin.y += page.y * contentSize.height 133 | return rect 134 | } 135 | 136 | private func elements(in rect: CGRect, page: CGPoint) -> [UICollectionViewLayoutAttributes] { 137 | let rect = self.rect(from: rect) 138 | let elements = super.layoutAttributesForElements(in: rect)? 139 | .map { self.layoutAttributes(from: $0, page: page) } 140 | .filter { $0 != nil } 141 | .map { $0! } ?? [] 142 | return elements 143 | } 144 | 145 | private func layoutAttributes(from layoutAttributes: UICollectionViewLayoutAttributes, page: CGPoint) -> UICollectionViewLayoutAttributes! { 146 | guard let attributes = self.copyLayoutAttributes(layoutAttributes) else { 147 | return nil 148 | } 149 | attributes.frame = rect(from: attributes.frame, page: page) 150 | return attributes 151 | } 152 | 153 | // MARK: Loop 154 | private func updateContentOffset(_ offset: CGPoint) { 155 | guard let collectionView = self.collectionView else { 156 | return 157 | } 158 | collectionView.contentOffset = offset 159 | collectionView.layoutIfNeeded() 160 | } 161 | private func preferredContentOffset(forContentOffset contentOffset: CGPoint) -> CGPoint { 162 | return rect(from: CGRect(origin: contentOffset, size: .zero), page: self.page(from: .zero, offset: multiplier / 2)).origin 163 | } 164 | 165 | public func loopCollectionViewIfNeeded() { 166 | guard let collectionView = self.collectionView, self.hasValidLayout else { 167 | return 168 | } 169 | let page = self.pageIndex(from: self.page(for: collectionView.contentOffset)) 170 | let offset = self.preferredContentOffset(forContentOffset: collectionView.contentOffset) 171 | if (page < 2 || page > self.multiplier - 2) && collectionView.contentOffset != offset { 172 | self.updateContentOffset(offset) 173 | } 174 | } 175 | 176 | // MARK: Paging 177 | public func collectionViewRect() -> CGRect? { 178 | guard let collectionView = self.collectionView else { 179 | return nil 180 | } 181 | let margins = UIEdgeInsets(top: collectionView.contentInset.top + collectionView.layoutMargins.top, 182 | left: collectionView.contentInset.left + collectionView.layoutMargins.left, 183 | bottom: collectionView.contentInset.bottom + collectionView.layoutMargins.bottom, 184 | right: collectionView.contentInset.right + collectionView.layoutMargins.right) 185 | 186 | var visibleRect = CGRect() 187 | visibleRect.origin.x = margins.left 188 | visibleRect.origin.y = margins.top 189 | visibleRect.size.width = collectionView.bounds.width - visibleRect.origin.x - margins.right 190 | visibleRect.size.height = collectionView.bounds.height - visibleRect.origin.y - margins.bottom 191 | return visibleRect 192 | } 193 | 194 | public func visibleCollectionViewRect() -> CGRect? { 195 | guard let collectionView = self.collectionView, 196 | var collectionViewRect = self.collectionViewRect() else { 197 | return nil 198 | } 199 | collectionViewRect.origin.x += collectionView.contentOffset.x 200 | collectionViewRect.origin.y += collectionView.contentOffset.y 201 | return collectionViewRect 202 | } 203 | 204 | public func visibleLayoutAttributes(at offset: CGPoint? = nil) -> [UICollectionViewLayoutAttributes] { 205 | guard let collectionView = self.collectionView else { 206 | return [] 207 | } 208 | return (self.layoutAttributesForElements(in: CGRect(origin: offset ?? collectionView.contentOffset, size: collectionView.frame.size)) ?? []) 209 | .sorted(by: { lhs, rhs in 210 | guard let lhs = self.centeredContentOffset(forRect: lhs.frame) else { 211 | return false 212 | } 213 | guard let rhs = self.centeredContentOffset(forRect: rhs.frame) else { 214 | return true 215 | } 216 | let value: (CGPoint)->CGFloat = { 217 | return self.scrollDirection == .horizontal ? abs(collectionView.contentOffset.x - $0.x) : abs(collectionView.contentOffset.y - $0.y) 218 | } 219 | return value(lhs) < value(rhs) 220 | }) 221 | } 222 | 223 | public func preferredVisibleLayoutAttributes(at offset: CGPoint? = nil, velocity: CGPoint = .zero, targetOffset: CGPoint? = nil, indexPath: IndexPath? = nil) -> UICollectionViewLayoutAttributes? { 224 | guard let currentOffset = self.collectionView?.contentOffset else { 225 | return nil 226 | } 227 | let direction: (CGPoint)->Bool = { 228 | return self.scrollDirection == .horizontal ? $0.x > currentOffset.x : $0.y > currentOffset.y 229 | } 230 | let velocity = self.scrollDirection == .horizontal ? velocity.x : velocity.y 231 | let targetDirection = direction(targetOffset ?? currentOffset) 232 | let attributes = self.visibleLayoutAttributes(at: offset) 233 | if let indexPath = indexPath, 234 | let attributes = attributes.first(where: { $0.indexPath == indexPath }) { 235 | return attributes 236 | } 237 | return attributes 238 | .first { attributes in 239 | guard let offset = self.centeredContentOffset(forRect: attributes.frame) else { 240 | return false 241 | } 242 | return direction(offset) == targetDirection || velocity == 0 243 | } 244 | } 245 | 246 | func centeredContentOffset(forRect rect: CGRect) -> CGPoint? { 247 | guard let collectionView = self.collectionView, 248 | let collectionRect = self.collectionViewRect() else { 249 | return nil 250 | } 251 | return CGPoint(x: self.scrollDirection == .horizontal ? abs(rect.midX - collectionRect.origin.x - collectionRect.width/2) : collectionView.contentOffset.x, 252 | y: self.scrollDirection == .vertical ? abs(rect.midY - collectionRect.origin.y - collectionRect.height/2) : collectionView.contentOffset.y) 253 | } 254 | 255 | public func centerCollectionView(withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { 256 | guard let collectionView = self.collectionView, self.hasValidLayout else { 257 | return 258 | } 259 | let newTarget = CGPoint(x: self.scrollDirection == .horizontal ? collectionView.contentOffset.x + velocity.x * velocityMultiplier : targetContentOffset.pointee.x, 260 | y: self.scrollDirection == .vertical ? collectionView.contentOffset.y + velocity.y * velocityMultiplier : targetContentOffset.pointee.y) 261 | 262 | guard let preferredAttributes = self.preferredVisibleLayoutAttributes(at: newTarget, velocity: velocity, targetOffset: targetContentOffset.pointee), 263 | let offset = self.centeredContentOffset(forRect: preferredAttributes.frame) else { 264 | return 265 | } 266 | targetContentOffset.pointee = offset 267 | } 268 | 269 | public func centerCollectionViewIfNeeded(indexPath: IndexPath? = nil) { 270 | guard let collectionView = self.collectionView, self.hasValidLayout else { 271 | return 272 | } 273 | guard let preferredAttributes = self.preferredVisibleLayoutAttributes(indexPath: indexPath), 274 | let offset = self.centeredContentOffset(forRect: preferredAttributes.frame), 275 | collectionView.contentOffset != offset else { 276 | return 277 | } 278 | self.updateContentOffset(offset) 279 | } 280 | 281 | // MARK: Copy 282 | public func copyLayoutAttributes(_ attributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes? { 283 | return attributes.copy() as? UICollectionViewLayoutAttributes 284 | } 285 | 286 | public func copyLayoutAttributes(from array: [UICollectionViewLayoutAttributes]) -> [UICollectionViewLayoutAttributes] { 287 | return array.map { self.copyLayoutAttributes($0) } 288 | .filter { $0 != nil } 289 | .map { $0! } 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /Sources/InfiniteLayout/SPMBridge.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InfiniteCollectionView.swift 3 | // InfiniteLayout 4 | // 5 | // Created by Arnaud Dorgans on 05/09/2019. 6 | // 7 | 8 | @_exported import CocoaProxy 9 | -------------------------------------------------------------------------------- /Sources/Rx/InfiniteCollectionView+Rx.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InfiniteCollectionView+Rx.swift 3 | // InfiniteLayout 4 | // 5 | // Created by Arnaud Dorgans on 03/01/2018. 6 | // 7 | 8 | #if canImport(InfiniteLayout) 9 | import InfiniteLayout 10 | #endif 11 | 12 | import UIKit 13 | import RxSwift 14 | import RxCocoa 15 | import RxDataSources 16 | 17 | class RxInfiniteCollectionViewDelegate: DelegateProxy, DelegateProxyType, InfiniteCollectionViewDelegate { 18 | 19 | init(infiniteCollectionView: InfiniteCollectionView) { 20 | super.init(parentObject: infiniteCollectionView, delegateProxy: RxInfiniteCollectionViewDelegate.self) 21 | } 22 | 23 | static func registerKnownImplementations() { 24 | RxInfiniteCollectionViewDelegate.register { 25 | RxInfiniteCollectionViewDelegate(infiniteCollectionView: $0) 26 | } 27 | } 28 | 29 | static func currentDelegate(for object: InfiniteCollectionView) -> InfiniteCollectionViewDelegate? { 30 | return object.infiniteDelegate 31 | } 32 | 33 | static func setCurrentDelegate(_ delegate: InfiniteCollectionViewDelegate?, to object: InfiniteCollectionView) { 34 | object.infiniteDelegate = delegate 35 | } 36 | } 37 | 38 | extension Reactive where Base: InfiniteCollectionView { 39 | 40 | private var infiniteDelegate: RxInfiniteCollectionViewDelegate { 41 | return RxInfiniteCollectionViewDelegate.proxy(for: self.base) 42 | } 43 | 44 | public var itemCentered: ControlEvent { 45 | let source = infiniteDelegate.sentMessage(#selector(InfiniteCollectionViewDelegate.infiniteCollectionView(_:didChangeCenteredIndexPath:to:))) 46 | .map { $0.last as? IndexPath } 47 | return ControlEvent(events: source) 48 | } 49 | 50 | public func modelCentered(_ type: T.Type) -> ControlEvent { 51 | let source: Observable = itemCentered.flatMap { [weak view = self.base as InfiniteCollectionView] indexPath -> Observable in 52 | guard let view = view, var indexPath = indexPath else { 53 | return Observable.empty() 54 | } 55 | 56 | indexPath.row %= InfiniteDataSources.originCount 57 | return Observable.just(try view.rx.model(at: indexPath)) 58 | } 59 | return ControlEvent(events: source) 60 | } 61 | 62 | public func modelSelected(_ modelType: T.Type) -> ControlEvent { 63 | let source: Observable = itemSelected.flatMap { [weak view = self.base as InfiniteCollectionView] indexPath -> Observable in 64 | guard let view = view else { 65 | return Observable.empty() 66 | } 67 | 68 | var indexPath = indexPath 69 | indexPath.row %= InfiniteDataSources.originCount 70 | return Observable.just(try view.rx.model(at: indexPath)) 71 | } 72 | 73 | return ControlEvent(events: source) 74 | } 75 | } 76 | 77 | extension Reactive where Base: RxInfiniteCollectionView { 78 | 79 | public func items 80 | (infinite: Bool) 81 | -> (_ source: O) 82 | -> (_ cellFactory: @escaping (UICollectionView, Int, S.Iterator.Element) -> UICollectionViewCell) 83 | -> Disposable where O.Element == S { 84 | return { source in 85 | guard infinite else { 86 | return self.items(source) 87 | } 88 | return { cellFactory in 89 | let dataSource = RxInfiniteCollectionViewSectionedReloadDataSource>(configureCell: { _, collectionView, indexPath, element in 90 | return cellFactory(collectionView, indexPath.item, element) 91 | }) 92 | return self.items(dataSource: dataSource)(source.map { [SectionModel(model: 0, items: $0.map { $0 })] }) 93 | } 94 | } 95 | } 96 | 97 | public func items 98 | (cellIdentifier: String, cellType: Cell.Type = Cell.self, infinite: Bool) 99 | -> (_ source: O) 100 | -> (_ configureCell: @escaping (Int, S.Iterator.Element, Cell) -> Void) 101 | -> Disposable where O.Element == S { 102 | guard infinite else { 103 | return self.items(cellIdentifier: cellIdentifier, cellType: cellType) 104 | } 105 | return { source in 106 | return { configureCell in 107 | let dataSource = RxInfiniteCollectionViewSectionedReloadDataSource>(configureCell: { _, collectionView, indexPath, element in 108 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath) as! Cell 109 | configureCell(indexPath.item, element, cell) 110 | return cell 111 | }) 112 | return self.items(dataSource: dataSource)(source.map { [SectionModel(model: 0, items: $0.map { $0 })] }) 113 | } 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Sources/Rx/RxInfiniteCollectionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RxInfiniteCollectionView.swift 3 | // InfiniteLayout 4 | // 5 | // Created by Arnaud Dorgans on 03/01/2018. 6 | // 7 | 8 | #if canImport(InfiniteLayout) 9 | import InfiniteLayout 10 | #endif 11 | 12 | import UIKit 13 | import RxSwift 14 | import RxCocoa 15 | 16 | open class RxInfiniteCollectionView: InfiniteCollectionView { 17 | 18 | let disposeBag = DisposeBag() 19 | 20 | override open var forwardDelegate: Bool { 21 | return false 22 | } 23 | 24 | public override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) { 25 | super.init(frame: frame, collectionViewLayout: layout) 26 | setupRx() 27 | } 28 | 29 | required public init?(coder aDecoder: NSCoder) { 30 | super.init(coder: aDecoder) 31 | } 32 | 33 | open override func awakeFromNib() { 34 | super.awakeFromNib() 35 | 36 | setupRx() 37 | } 38 | 39 | func setupRx() { 40 | 41 | self.rx.setDelegate(self) 42 | .disposed(by: disposeBag) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/Rx/RxInfiniteCollectionViewDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RxInfiniteCollectionViewDelegate.swift 3 | // InfiniteLayout 4 | // 5 | // Created by Arnaud Dorgans on 03/01/2018. 6 | // 7 | 8 | #if canImport(InfiniteLayout) 9 | import InfiniteLayout 10 | #endif 11 | 12 | import UIKit 13 | import RxDataSources 14 | 15 | open class RxInfiniteCollectionViewSectionedReloadDataSource: RxCollectionViewSectionedReloadDataSource { 16 | 17 | public var isEnabled: Bool = true 18 | 19 | open override subscript(section: Int) -> S { 20 | let section = InfiniteDataSources.section(from: section, numberOfSections: sectionModels.count) 21 | return self.sectionModels[section] 22 | } 23 | 24 | open override subscript(indexPath: IndexPath) -> Item { 25 | get { 26 | let indexPath = InfiniteDataSources.indexPath(from: indexPath, 27 | numberOfSections: sectionModels.count, 28 | numberOfItems: self[indexPath.section].items.count) 29 | return super[indexPath] 30 | } set { 31 | let indexPath = InfiniteDataSources.indexPath(from: indexPath, 32 | numberOfSections: sectionModels.count, 33 | numberOfItems: self[indexPath.section].items.count) 34 | super[indexPath] = newValue 35 | } 36 | } 37 | 38 | private func multiplier(for collectionView: UICollectionView) -> Int { 39 | guard let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout else { 40 | fatalError() 41 | } 42 | return InfiniteDataSources.multiplier(estimatedItemSize: layout.itemSize, enabled: isEnabled) 43 | } 44 | 45 | open override func numberOfSections(in collectionView: UICollectionView) -> Int { 46 | return InfiniteDataSources.numberOfSections(numberOfSections: self.sectionModels.count, 47 | multiplier: self.multiplier(for: collectionView)) 48 | } 49 | 50 | open override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 51 | return InfiniteDataSources.numberOfItemsInSection(numberOfItemsInSection: self[section].items.count, 52 | numberOfSections: self.sectionModels.count, 53 | multiplier: self.multiplier(for: collectionView)) 54 | } 55 | 56 | open override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 57 | return configureCell(self, collectionView, indexPath, self[indexPath]) 58 | } 59 | } 60 | 61 | open class RxInfiniteCollectionViewSectionedAnimatedDataSource: RxCollectionViewSectionedAnimatedDataSource { 62 | 63 | public var isEnabled: Bool = true 64 | 65 | open override subscript(section: Int) -> S { 66 | let section = InfiniteDataSources.section(from: section, numberOfSections: sectionModels.count) 67 | return self.sectionModels[section] 68 | } 69 | 70 | open override subscript(indexPath: IndexPath) -> Item { 71 | get { 72 | let indexPath = InfiniteDataSources.indexPath(from: indexPath, 73 | numberOfSections: sectionModels.count, 74 | numberOfItems: self[indexPath.section].items.count) 75 | return super[indexPath] 76 | } set { 77 | let indexPath = InfiniteDataSources.indexPath(from: indexPath, 78 | numberOfSections: sectionModels.count, 79 | numberOfItems: self[indexPath.section].items.count) 80 | super[indexPath] = newValue 81 | } 82 | } 83 | 84 | private func multiplier(for collectionView: UICollectionView) -> Int { 85 | guard let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout else { 86 | fatalError() 87 | } 88 | return InfiniteDataSources.multiplier(estimatedItemSize: layout.itemSize, enabled: isEnabled) 89 | } 90 | 91 | open override func numberOfSections(in collectionView: UICollectionView) -> Int { 92 | return InfiniteDataSources.numberOfSections(numberOfSections: self.sectionModels.count, 93 | multiplier: self.multiplier(for: collectionView)) 94 | } 95 | 96 | open override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 97 | return InfiniteDataSources.numberOfItemsInSection(numberOfItemsInSection: self[section].items.count, 98 | numberOfSections: self.sectionModels.count, 99 | multiplier: self.multiplier(for: collectionView)) 100 | } 101 | 102 | open override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 103 | return configureCell(self, collectionView, indexPath, self[indexPath]) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /_Pods.xcodeproj: -------------------------------------------------------------------------------- 1 | Example/Pods/Pods.xcodeproj -------------------------------------------------------------------------------- /custom.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arnauddorgans/InfiniteLayout/a576a83530a94ccb3367ab12c8eac7ec5520e8c1/custom.gif -------------------------------------------------------------------------------- /delegate.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arnauddorgans/InfiniteLayout/a576a83530a94ccb3367ab12c8eac7ec5520e8c1/delegate.gif -------------------------------------------------------------------------------- /horizontal.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arnauddorgans/InfiniteLayout/a576a83530a94ccb3367ab12c8eac7ec5520e8c1/horizontal.gif -------------------------------------------------------------------------------- /vertical.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arnauddorgans/InfiniteLayout/a576a83530a94ccb3367ab12c8eac7ec5520e8c1/vertical.gif --------------------------------------------------------------------------------