├── .gitignore ├── Demo.gif ├── InfiniteAutoScrollWithCompositionalLayout.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── InfiniteAutoScrollWithCompositionalLayout ├── AppDelegate.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── 1024.png │ │ ├── 120-1.png │ │ ├── 120.png │ │ ├── 180.png │ │ ├── 40.png │ │ ├── 58.png │ │ ├── 60.png │ │ ├── 80.png │ │ ├── 87.png │ │ └── Contents.json │ ├── Contents.json │ ├── photo_1.imageset │ │ ├── Contents.json │ │ └── photo_1.jpg │ ├── photo_2.imageset │ │ ├── Contents.json │ │ └── photo_2.jpg │ ├── photo_3.imageset │ │ ├── Contents.json │ │ └── photo_3.jpg │ ├── photo_4.imageset │ │ ├── Contents.json │ │ └── photo_4.jpg │ └── photo_5.imageset │ │ ├── Contents.json │ │ └── photo_5.jpg ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── InfiniteAutoScrollView.swift ├── InfiniteAutoScrollViewCell.swift ├── Info.plist ├── SceneDelegate.swift ├── UIDevice+Extension.swift └── ViewController.swift ├── InfiniteAutoScrollWithCompositionalLayoutTests └── InfiniteAutoScrollWithCompositionalLayoutTests.swift ├── InfiniteAutoScrollWithCompositionalLayoutUITests ├── InfiniteAutoScrollWithCompositionalLayoutUITests.swift └── InfiniteAutoScrollWithCompositionalLayoutUITestsLaunchTests.swift ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | -------------------------------------------------------------------------------- /Demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rachelpeichen/Infinite_Auto_Scrolling_CollectionView_CompositionalLayout/8300284d8a7e403b3f27269d1151210ce7f6e6e1/Demo.gif -------------------------------------------------------------------------------- /InfiniteAutoScrollWithCompositionalLayout.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | D1300C5C282E757100122349 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1300C5B282E757100122349 /* AppDelegate.swift */; }; 11 | D1300C5E282E757100122349 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1300C5D282E757100122349 /* SceneDelegate.swift */; }; 12 | D1300C60282E757100122349 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1300C5F282E757100122349 /* ViewController.swift */; }; 13 | D1300C63282E757100122349 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D1300C61282E757100122349 /* Main.storyboard */; }; 14 | D1300C65282E757100122349 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D1300C64282E757100122349 /* Assets.xcassets */; }; 15 | D1300C68282E757100122349 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D1300C66282E757100122349 /* LaunchScreen.storyboard */; }; 16 | D1300C73282E757200122349 /* InfiniteAutoScrollWithCompositionalLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1300C72282E757200122349 /* InfiniteAutoScrollWithCompositionalLayoutTests.swift */; }; 17 | D1300C7D282E757200122349 /* InfiniteAutoScrollWithCompositionalLayoutUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1300C7C282E757200122349 /* InfiniteAutoScrollWithCompositionalLayoutUITests.swift */; }; 18 | D1300C7F282E757200122349 /* InfiniteAutoScrollWithCompositionalLayoutUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1300C7E282E757200122349 /* InfiniteAutoScrollWithCompositionalLayoutUITestsLaunchTests.swift */; }; 19 | D1300C8C282E75BF00122349 /* InfiniteAutoScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1300C8B282E75BF00122349 /* InfiniteAutoScrollView.swift */; }; 20 | D1300C8E282E761B00122349 /* InfiniteAutoScrollViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1300C8D282E761B00122349 /* InfiniteAutoScrollViewCell.swift */; }; 21 | D1300C9928337FAC00122349 /* UIDevice+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1300C9828337FAC00122349 /* UIDevice+Extension.swift */; }; 22 | /* End PBXBuildFile section */ 23 | 24 | /* Begin PBXContainerItemProxy section */ 25 | D1300C6F282E757200122349 /* PBXContainerItemProxy */ = { 26 | isa = PBXContainerItemProxy; 27 | containerPortal = D1300C50282E757100122349 /* Project object */; 28 | proxyType = 1; 29 | remoteGlobalIDString = D1300C57282E757100122349; 30 | remoteInfo = InfiniteAutoScrollWithCompositionalLayout; 31 | }; 32 | D1300C79282E757200122349 /* PBXContainerItemProxy */ = { 33 | isa = PBXContainerItemProxy; 34 | containerPortal = D1300C50282E757100122349 /* Project object */; 35 | proxyType = 1; 36 | remoteGlobalIDString = D1300C57282E757100122349; 37 | remoteInfo = InfiniteAutoScrollWithCompositionalLayout; 38 | }; 39 | /* End PBXContainerItemProxy section */ 40 | 41 | /* Begin PBXFileReference section */ 42 | D1300C58282E757100122349 /* InfiniteAutoScrollWithCompositionalLayout.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = InfiniteAutoScrollWithCompositionalLayout.app; sourceTree = BUILT_PRODUCTS_DIR; }; 43 | D1300C5B282E757100122349 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 44 | D1300C5D282E757100122349 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 45 | D1300C5F282E757100122349 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 46 | D1300C62282E757100122349 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 47 | D1300C64282E757100122349 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 48 | D1300C67282E757100122349 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 49 | D1300C69282E757100122349 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 50 | D1300C6E282E757200122349 /* InfiniteAutoScrollWithCompositionalLayoutTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = InfiniteAutoScrollWithCompositionalLayoutTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 51 | D1300C72282E757200122349 /* InfiniteAutoScrollWithCompositionalLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfiniteAutoScrollWithCompositionalLayoutTests.swift; sourceTree = ""; }; 52 | D1300C78282E757200122349 /* InfiniteAutoScrollWithCompositionalLayoutUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = InfiniteAutoScrollWithCompositionalLayoutUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 53 | D1300C7C282E757200122349 /* InfiniteAutoScrollWithCompositionalLayoutUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfiniteAutoScrollWithCompositionalLayoutUITests.swift; sourceTree = ""; }; 54 | D1300C7E282E757200122349 /* InfiniteAutoScrollWithCompositionalLayoutUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfiniteAutoScrollWithCompositionalLayoutUITestsLaunchTests.swift; sourceTree = ""; }; 55 | D1300C8B282E75BF00122349 /* InfiniteAutoScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfiniteAutoScrollView.swift; sourceTree = ""; }; 56 | D1300C8D282E761B00122349 /* InfiniteAutoScrollViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfiniteAutoScrollViewCell.swift; sourceTree = ""; }; 57 | D1300C9828337FAC00122349 /* UIDevice+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIDevice+Extension.swift"; sourceTree = ""; }; 58 | /* End PBXFileReference section */ 59 | 60 | /* Begin PBXFrameworksBuildPhase section */ 61 | D1300C55282E757100122349 /* Frameworks */ = { 62 | isa = PBXFrameworksBuildPhase; 63 | buildActionMask = 2147483647; 64 | files = ( 65 | ); 66 | runOnlyForDeploymentPostprocessing = 0; 67 | }; 68 | D1300C6B282E757200122349 /* Frameworks */ = { 69 | isa = PBXFrameworksBuildPhase; 70 | buildActionMask = 2147483647; 71 | files = ( 72 | ); 73 | runOnlyForDeploymentPostprocessing = 0; 74 | }; 75 | D1300C75282E757200122349 /* Frameworks */ = { 76 | isa = PBXFrameworksBuildPhase; 77 | buildActionMask = 2147483647; 78 | files = ( 79 | ); 80 | runOnlyForDeploymentPostprocessing = 0; 81 | }; 82 | /* End PBXFrameworksBuildPhase section */ 83 | 84 | /* Begin PBXGroup section */ 85 | D1300C4F282E757100122349 = { 86 | isa = PBXGroup; 87 | children = ( 88 | D1300C5A282E757100122349 /* InfiniteAutoScrollWithCompositionalLayout */, 89 | D1300C71282E757200122349 /* InfiniteAutoScrollWithCompositionalLayoutTests */, 90 | D1300C7B282E757200122349 /* InfiniteAutoScrollWithCompositionalLayoutUITests */, 91 | D1300C59282E757100122349 /* Products */, 92 | ); 93 | sourceTree = ""; 94 | }; 95 | D1300C59282E757100122349 /* Products */ = { 96 | isa = PBXGroup; 97 | children = ( 98 | D1300C58282E757100122349 /* InfiniteAutoScrollWithCompositionalLayout.app */, 99 | D1300C6E282E757200122349 /* InfiniteAutoScrollWithCompositionalLayoutTests.xctest */, 100 | D1300C78282E757200122349 /* InfiniteAutoScrollWithCompositionalLayoutUITests.xctest */, 101 | ); 102 | name = Products; 103 | sourceTree = ""; 104 | }; 105 | D1300C5A282E757100122349 /* InfiniteAutoScrollWithCompositionalLayout */ = { 106 | isa = PBXGroup; 107 | children = ( 108 | D1300C8B282E75BF00122349 /* InfiniteAutoScrollView.swift */, 109 | D1300C8D282E761B00122349 /* InfiniteAutoScrollViewCell.swift */, 110 | D1300C5F282E757100122349 /* ViewController.swift */, 111 | D1300C5B282E757100122349 /* AppDelegate.swift */, 112 | D1300C5D282E757100122349 /* SceneDelegate.swift */, 113 | D1300C61282E757100122349 /* Main.storyboard */, 114 | D1300C64282E757100122349 /* Assets.xcassets */, 115 | D1300C66282E757100122349 /* LaunchScreen.storyboard */, 116 | D1300C69282E757100122349 /* Info.plist */, 117 | D1300C9828337FAC00122349 /* UIDevice+Extension.swift */, 118 | ); 119 | path = InfiniteAutoScrollWithCompositionalLayout; 120 | sourceTree = ""; 121 | }; 122 | D1300C71282E757200122349 /* InfiniteAutoScrollWithCompositionalLayoutTests */ = { 123 | isa = PBXGroup; 124 | children = ( 125 | D1300C72282E757200122349 /* InfiniteAutoScrollWithCompositionalLayoutTests.swift */, 126 | ); 127 | path = InfiniteAutoScrollWithCompositionalLayoutTests; 128 | sourceTree = ""; 129 | }; 130 | D1300C7B282E757200122349 /* InfiniteAutoScrollWithCompositionalLayoutUITests */ = { 131 | isa = PBXGroup; 132 | children = ( 133 | D1300C7C282E757200122349 /* InfiniteAutoScrollWithCompositionalLayoutUITests.swift */, 134 | D1300C7E282E757200122349 /* InfiniteAutoScrollWithCompositionalLayoutUITestsLaunchTests.swift */, 135 | ); 136 | path = InfiniteAutoScrollWithCompositionalLayoutUITests; 137 | sourceTree = ""; 138 | }; 139 | /* End PBXGroup section */ 140 | 141 | /* Begin PBXNativeTarget section */ 142 | D1300C57282E757100122349 /* InfiniteAutoScrollWithCompositionalLayout */ = { 143 | isa = PBXNativeTarget; 144 | buildConfigurationList = D1300C82282E757200122349 /* Build configuration list for PBXNativeTarget "InfiniteAutoScrollWithCompositionalLayout" */; 145 | buildPhases = ( 146 | D1300C54282E757100122349 /* Sources */, 147 | D1300C55282E757100122349 /* Frameworks */, 148 | D1300C56282E757100122349 /* Resources */, 149 | ); 150 | buildRules = ( 151 | ); 152 | dependencies = ( 153 | ); 154 | name = InfiniteAutoScrollWithCompositionalLayout; 155 | productName = InfiniteAutoScrollWithCompositionalLayout; 156 | productReference = D1300C58282E757100122349 /* InfiniteAutoScrollWithCompositionalLayout.app */; 157 | productType = "com.apple.product-type.application"; 158 | }; 159 | D1300C6D282E757200122349 /* InfiniteAutoScrollWithCompositionalLayoutTests */ = { 160 | isa = PBXNativeTarget; 161 | buildConfigurationList = D1300C85282E757200122349 /* Build configuration list for PBXNativeTarget "InfiniteAutoScrollWithCompositionalLayoutTests" */; 162 | buildPhases = ( 163 | D1300C6A282E757200122349 /* Sources */, 164 | D1300C6B282E757200122349 /* Frameworks */, 165 | D1300C6C282E757200122349 /* Resources */, 166 | ); 167 | buildRules = ( 168 | ); 169 | dependencies = ( 170 | D1300C70282E757200122349 /* PBXTargetDependency */, 171 | ); 172 | name = InfiniteAutoScrollWithCompositionalLayoutTests; 173 | productName = InfiniteAutoScrollWithCompositionalLayoutTests; 174 | productReference = D1300C6E282E757200122349 /* InfiniteAutoScrollWithCompositionalLayoutTests.xctest */; 175 | productType = "com.apple.product-type.bundle.unit-test"; 176 | }; 177 | D1300C77282E757200122349 /* InfiniteAutoScrollWithCompositionalLayoutUITests */ = { 178 | isa = PBXNativeTarget; 179 | buildConfigurationList = D1300C88282E757200122349 /* Build configuration list for PBXNativeTarget "InfiniteAutoScrollWithCompositionalLayoutUITests" */; 180 | buildPhases = ( 181 | D1300C74282E757200122349 /* Sources */, 182 | D1300C75282E757200122349 /* Frameworks */, 183 | D1300C76282E757200122349 /* Resources */, 184 | ); 185 | buildRules = ( 186 | ); 187 | dependencies = ( 188 | D1300C7A282E757200122349 /* PBXTargetDependency */, 189 | ); 190 | name = InfiniteAutoScrollWithCompositionalLayoutUITests; 191 | productName = InfiniteAutoScrollWithCompositionalLayoutUITests; 192 | productReference = D1300C78282E757200122349 /* InfiniteAutoScrollWithCompositionalLayoutUITests.xctest */; 193 | productType = "com.apple.product-type.bundle.ui-testing"; 194 | }; 195 | /* End PBXNativeTarget section */ 196 | 197 | /* Begin PBXProject section */ 198 | D1300C50282E757100122349 /* Project object */ = { 199 | isa = PBXProject; 200 | attributes = { 201 | BuildIndependentTargetsInParallel = 1; 202 | LastSwiftUpdateCheck = 1330; 203 | LastUpgradeCheck = 1330; 204 | TargetAttributes = { 205 | D1300C57282E757100122349 = { 206 | CreatedOnToolsVersion = 13.3.1; 207 | }; 208 | D1300C6D282E757200122349 = { 209 | CreatedOnToolsVersion = 13.3.1; 210 | TestTargetID = D1300C57282E757100122349; 211 | }; 212 | D1300C77282E757200122349 = { 213 | CreatedOnToolsVersion = 13.3.1; 214 | TestTargetID = D1300C57282E757100122349; 215 | }; 216 | }; 217 | }; 218 | buildConfigurationList = D1300C53282E757100122349 /* Build configuration list for PBXProject "InfiniteAutoScrollWithCompositionalLayout" */; 219 | compatibilityVersion = "Xcode 13.0"; 220 | developmentRegion = en; 221 | hasScannedForEncodings = 0; 222 | knownRegions = ( 223 | en, 224 | Base, 225 | ); 226 | mainGroup = D1300C4F282E757100122349; 227 | productRefGroup = D1300C59282E757100122349 /* Products */; 228 | projectDirPath = ""; 229 | projectRoot = ""; 230 | targets = ( 231 | D1300C57282E757100122349 /* InfiniteAutoScrollWithCompositionalLayout */, 232 | D1300C6D282E757200122349 /* InfiniteAutoScrollWithCompositionalLayoutTests */, 233 | D1300C77282E757200122349 /* InfiniteAutoScrollWithCompositionalLayoutUITests */, 234 | ); 235 | }; 236 | /* End PBXProject section */ 237 | 238 | /* Begin PBXResourcesBuildPhase section */ 239 | D1300C56282E757100122349 /* Resources */ = { 240 | isa = PBXResourcesBuildPhase; 241 | buildActionMask = 2147483647; 242 | files = ( 243 | D1300C68282E757100122349 /* LaunchScreen.storyboard in Resources */, 244 | D1300C65282E757100122349 /* Assets.xcassets in Resources */, 245 | D1300C63282E757100122349 /* Main.storyboard in Resources */, 246 | ); 247 | runOnlyForDeploymentPostprocessing = 0; 248 | }; 249 | D1300C6C282E757200122349 /* Resources */ = { 250 | isa = PBXResourcesBuildPhase; 251 | buildActionMask = 2147483647; 252 | files = ( 253 | ); 254 | runOnlyForDeploymentPostprocessing = 0; 255 | }; 256 | D1300C76282E757200122349 /* Resources */ = { 257 | isa = PBXResourcesBuildPhase; 258 | buildActionMask = 2147483647; 259 | files = ( 260 | ); 261 | runOnlyForDeploymentPostprocessing = 0; 262 | }; 263 | /* End PBXResourcesBuildPhase section */ 264 | 265 | /* Begin PBXSourcesBuildPhase section */ 266 | D1300C54282E757100122349 /* Sources */ = { 267 | isa = PBXSourcesBuildPhase; 268 | buildActionMask = 2147483647; 269 | files = ( 270 | D1300C8C282E75BF00122349 /* InfiniteAutoScrollView.swift in Sources */, 271 | D1300C9928337FAC00122349 /* UIDevice+Extension.swift in Sources */, 272 | D1300C60282E757100122349 /* ViewController.swift in Sources */, 273 | D1300C5C282E757100122349 /* AppDelegate.swift in Sources */, 274 | D1300C8E282E761B00122349 /* InfiniteAutoScrollViewCell.swift in Sources */, 275 | D1300C5E282E757100122349 /* SceneDelegate.swift in Sources */, 276 | ); 277 | runOnlyForDeploymentPostprocessing = 0; 278 | }; 279 | D1300C6A282E757200122349 /* Sources */ = { 280 | isa = PBXSourcesBuildPhase; 281 | buildActionMask = 2147483647; 282 | files = ( 283 | D1300C73282E757200122349 /* InfiniteAutoScrollWithCompositionalLayoutTests.swift in Sources */, 284 | ); 285 | runOnlyForDeploymentPostprocessing = 0; 286 | }; 287 | D1300C74282E757200122349 /* Sources */ = { 288 | isa = PBXSourcesBuildPhase; 289 | buildActionMask = 2147483647; 290 | files = ( 291 | D1300C7F282E757200122349 /* InfiniteAutoScrollWithCompositionalLayoutUITestsLaunchTests.swift in Sources */, 292 | D1300C7D282E757200122349 /* InfiniteAutoScrollWithCompositionalLayoutUITests.swift in Sources */, 293 | ); 294 | runOnlyForDeploymentPostprocessing = 0; 295 | }; 296 | /* End PBXSourcesBuildPhase section */ 297 | 298 | /* Begin PBXTargetDependency section */ 299 | D1300C70282E757200122349 /* PBXTargetDependency */ = { 300 | isa = PBXTargetDependency; 301 | target = D1300C57282E757100122349 /* InfiniteAutoScrollWithCompositionalLayout */; 302 | targetProxy = D1300C6F282E757200122349 /* PBXContainerItemProxy */; 303 | }; 304 | D1300C7A282E757200122349 /* PBXTargetDependency */ = { 305 | isa = PBXTargetDependency; 306 | target = D1300C57282E757100122349 /* InfiniteAutoScrollWithCompositionalLayout */; 307 | targetProxy = D1300C79282E757200122349 /* PBXContainerItemProxy */; 308 | }; 309 | /* End PBXTargetDependency section */ 310 | 311 | /* Begin PBXVariantGroup section */ 312 | D1300C61282E757100122349 /* Main.storyboard */ = { 313 | isa = PBXVariantGroup; 314 | children = ( 315 | D1300C62282E757100122349 /* Base */, 316 | ); 317 | name = Main.storyboard; 318 | sourceTree = ""; 319 | }; 320 | D1300C66282E757100122349 /* LaunchScreen.storyboard */ = { 321 | isa = PBXVariantGroup; 322 | children = ( 323 | D1300C67282E757100122349 /* Base */, 324 | ); 325 | name = LaunchScreen.storyboard; 326 | sourceTree = ""; 327 | }; 328 | /* End PBXVariantGroup section */ 329 | 330 | /* Begin XCBuildConfiguration section */ 331 | D1300C80282E757200122349 /* Debug */ = { 332 | isa = XCBuildConfiguration; 333 | buildSettings = { 334 | ALWAYS_SEARCH_USER_PATHS = NO; 335 | CLANG_ANALYZER_NONNULL = YES; 336 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 337 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 338 | CLANG_ENABLE_MODULES = YES; 339 | CLANG_ENABLE_OBJC_ARC = YES; 340 | CLANG_ENABLE_OBJC_WEAK = YES; 341 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 342 | CLANG_WARN_BOOL_CONVERSION = YES; 343 | CLANG_WARN_COMMA = YES; 344 | CLANG_WARN_CONSTANT_CONVERSION = YES; 345 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 346 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 347 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 348 | CLANG_WARN_EMPTY_BODY = YES; 349 | CLANG_WARN_ENUM_CONVERSION = YES; 350 | CLANG_WARN_INFINITE_RECURSION = YES; 351 | CLANG_WARN_INT_CONVERSION = YES; 352 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 353 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 354 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 355 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 356 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 357 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 358 | CLANG_WARN_STRICT_PROTOTYPES = YES; 359 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 360 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 361 | CLANG_WARN_UNREACHABLE_CODE = YES; 362 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 363 | COPY_PHASE_STRIP = NO; 364 | DEBUG_INFORMATION_FORMAT = dwarf; 365 | ENABLE_STRICT_OBJC_MSGSEND = YES; 366 | ENABLE_TESTABILITY = YES; 367 | GCC_C_LANGUAGE_STANDARD = gnu11; 368 | GCC_DYNAMIC_NO_PIC = NO; 369 | GCC_NO_COMMON_BLOCKS = YES; 370 | GCC_OPTIMIZATION_LEVEL = 0; 371 | GCC_PREPROCESSOR_DEFINITIONS = ( 372 | "DEBUG=1", 373 | "$(inherited)", 374 | ); 375 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 376 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 377 | GCC_WARN_UNDECLARED_SELECTOR = YES; 378 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 379 | GCC_WARN_UNUSED_FUNCTION = YES; 380 | GCC_WARN_UNUSED_VARIABLE = YES; 381 | IPHONEOS_DEPLOYMENT_TARGET = 15.4; 382 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 383 | MTL_FAST_MATH = YES; 384 | ONLY_ACTIVE_ARCH = YES; 385 | SDKROOT = iphoneos; 386 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 387 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 388 | }; 389 | name = Debug; 390 | }; 391 | D1300C81282E757200122349 /* Release */ = { 392 | isa = XCBuildConfiguration; 393 | buildSettings = { 394 | ALWAYS_SEARCH_USER_PATHS = NO; 395 | CLANG_ANALYZER_NONNULL = YES; 396 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 397 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 398 | CLANG_ENABLE_MODULES = YES; 399 | CLANG_ENABLE_OBJC_ARC = YES; 400 | CLANG_ENABLE_OBJC_WEAK = YES; 401 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 402 | CLANG_WARN_BOOL_CONVERSION = YES; 403 | CLANG_WARN_COMMA = YES; 404 | CLANG_WARN_CONSTANT_CONVERSION = YES; 405 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 406 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 407 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 408 | CLANG_WARN_EMPTY_BODY = YES; 409 | CLANG_WARN_ENUM_CONVERSION = YES; 410 | CLANG_WARN_INFINITE_RECURSION = YES; 411 | CLANG_WARN_INT_CONVERSION = YES; 412 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 413 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 414 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 415 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 416 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 417 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 418 | CLANG_WARN_STRICT_PROTOTYPES = YES; 419 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 420 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 421 | CLANG_WARN_UNREACHABLE_CODE = YES; 422 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 423 | COPY_PHASE_STRIP = NO; 424 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 425 | ENABLE_NS_ASSERTIONS = NO; 426 | ENABLE_STRICT_OBJC_MSGSEND = YES; 427 | GCC_C_LANGUAGE_STANDARD = gnu11; 428 | GCC_NO_COMMON_BLOCKS = YES; 429 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 430 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 431 | GCC_WARN_UNDECLARED_SELECTOR = YES; 432 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 433 | GCC_WARN_UNUSED_FUNCTION = YES; 434 | GCC_WARN_UNUSED_VARIABLE = YES; 435 | IPHONEOS_DEPLOYMENT_TARGET = 15.4; 436 | MTL_ENABLE_DEBUG_INFO = NO; 437 | MTL_FAST_MATH = YES; 438 | SDKROOT = iphoneos; 439 | SWIFT_COMPILATION_MODE = wholemodule; 440 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 441 | VALIDATE_PRODUCT = YES; 442 | }; 443 | name = Release; 444 | }; 445 | D1300C83282E757200122349 /* Debug */ = { 446 | isa = XCBuildConfiguration; 447 | buildSettings = { 448 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 449 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 450 | CODE_SIGN_STYLE = Automatic; 451 | CURRENT_PROJECT_VERSION = 1; 452 | DEVELOPMENT_TEAM = P4J3B539A4; 453 | GENERATE_INFOPLIST_FILE = YES; 454 | INFOPLIST_FILE = InfiniteAutoScrollWithCompositionalLayout/Info.plist; 455 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 456 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 457 | INFOPLIST_KEY_UIMainStoryboardFile = Main; 458 | INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; 459 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 460 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 461 | LD_RUNPATH_SEARCH_PATHS = ( 462 | "$(inherited)", 463 | "@executable_path/Frameworks", 464 | ); 465 | MARKETING_VERSION = 1.0; 466 | PRODUCT_BUNDLE_IDENTIFIER = com.hpchen.InfiniteAutoScrollWithCompositionalLayout; 467 | PRODUCT_NAME = "$(TARGET_NAME)"; 468 | SWIFT_EMIT_LOC_STRINGS = YES; 469 | SWIFT_VERSION = 5.0; 470 | TARGETED_DEVICE_FAMILY = 1; 471 | }; 472 | name = Debug; 473 | }; 474 | D1300C84282E757200122349 /* Release */ = { 475 | isa = XCBuildConfiguration; 476 | buildSettings = { 477 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 478 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 479 | CODE_SIGN_STYLE = Automatic; 480 | CURRENT_PROJECT_VERSION = 1; 481 | DEVELOPMENT_TEAM = P4J3B539A4; 482 | GENERATE_INFOPLIST_FILE = YES; 483 | INFOPLIST_FILE = InfiniteAutoScrollWithCompositionalLayout/Info.plist; 484 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 485 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 486 | INFOPLIST_KEY_UIMainStoryboardFile = Main; 487 | INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; 488 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 489 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 490 | LD_RUNPATH_SEARCH_PATHS = ( 491 | "$(inherited)", 492 | "@executable_path/Frameworks", 493 | ); 494 | MARKETING_VERSION = 1.0; 495 | PRODUCT_BUNDLE_IDENTIFIER = com.hpchen.InfiniteAutoScrollWithCompositionalLayout; 496 | PRODUCT_NAME = "$(TARGET_NAME)"; 497 | SWIFT_EMIT_LOC_STRINGS = YES; 498 | SWIFT_VERSION = 5.0; 499 | TARGETED_DEVICE_FAMILY = 1; 500 | }; 501 | name = Release; 502 | }; 503 | D1300C86282E757200122349 /* Debug */ = { 504 | isa = XCBuildConfiguration; 505 | buildSettings = { 506 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 507 | BUNDLE_LOADER = "$(TEST_HOST)"; 508 | CODE_SIGN_STYLE = Automatic; 509 | CURRENT_PROJECT_VERSION = 1; 510 | DEVELOPMENT_TEAM = P4J3B539A4; 511 | GENERATE_INFOPLIST_FILE = YES; 512 | IPHONEOS_DEPLOYMENT_TARGET = 15.4; 513 | MARKETING_VERSION = 1.0; 514 | PRODUCT_BUNDLE_IDENTIFIER = com.hpchen.InfiniteAutoScrollWithCompositionalLayoutTests; 515 | PRODUCT_NAME = "$(TARGET_NAME)"; 516 | SWIFT_EMIT_LOC_STRINGS = NO; 517 | SWIFT_VERSION = 5.0; 518 | TARGETED_DEVICE_FAMILY = "1,2"; 519 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/InfiniteAutoScrollWithCompositionalLayout.app/InfiniteAutoScrollWithCompositionalLayout"; 520 | }; 521 | name = Debug; 522 | }; 523 | D1300C87282E757200122349 /* Release */ = { 524 | isa = XCBuildConfiguration; 525 | buildSettings = { 526 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 527 | BUNDLE_LOADER = "$(TEST_HOST)"; 528 | CODE_SIGN_STYLE = Automatic; 529 | CURRENT_PROJECT_VERSION = 1; 530 | DEVELOPMENT_TEAM = P4J3B539A4; 531 | GENERATE_INFOPLIST_FILE = YES; 532 | IPHONEOS_DEPLOYMENT_TARGET = 15.4; 533 | MARKETING_VERSION = 1.0; 534 | PRODUCT_BUNDLE_IDENTIFIER = com.hpchen.InfiniteAutoScrollWithCompositionalLayoutTests; 535 | PRODUCT_NAME = "$(TARGET_NAME)"; 536 | SWIFT_EMIT_LOC_STRINGS = NO; 537 | SWIFT_VERSION = 5.0; 538 | TARGETED_DEVICE_FAMILY = "1,2"; 539 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/InfiniteAutoScrollWithCompositionalLayout.app/InfiniteAutoScrollWithCompositionalLayout"; 540 | }; 541 | name = Release; 542 | }; 543 | D1300C89282E757200122349 /* Debug */ = { 544 | isa = XCBuildConfiguration; 545 | buildSettings = { 546 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 547 | CODE_SIGN_STYLE = Automatic; 548 | CURRENT_PROJECT_VERSION = 1; 549 | DEVELOPMENT_TEAM = P4J3B539A4; 550 | GENERATE_INFOPLIST_FILE = YES; 551 | MARKETING_VERSION = 1.0; 552 | PRODUCT_BUNDLE_IDENTIFIER = com.hpchen.InfiniteAutoScrollWithCompositionalLayoutUITests; 553 | PRODUCT_NAME = "$(TARGET_NAME)"; 554 | SWIFT_EMIT_LOC_STRINGS = NO; 555 | SWIFT_VERSION = 5.0; 556 | TARGETED_DEVICE_FAMILY = "1,2"; 557 | TEST_TARGET_NAME = InfiniteAutoScrollWithCompositionalLayout; 558 | }; 559 | name = Debug; 560 | }; 561 | D1300C8A282E757200122349 /* Release */ = { 562 | isa = XCBuildConfiguration; 563 | buildSettings = { 564 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 565 | CODE_SIGN_STYLE = Automatic; 566 | CURRENT_PROJECT_VERSION = 1; 567 | DEVELOPMENT_TEAM = P4J3B539A4; 568 | GENERATE_INFOPLIST_FILE = YES; 569 | MARKETING_VERSION = 1.0; 570 | PRODUCT_BUNDLE_IDENTIFIER = com.hpchen.InfiniteAutoScrollWithCompositionalLayoutUITests; 571 | PRODUCT_NAME = "$(TARGET_NAME)"; 572 | SWIFT_EMIT_LOC_STRINGS = NO; 573 | SWIFT_VERSION = 5.0; 574 | TARGETED_DEVICE_FAMILY = "1,2"; 575 | TEST_TARGET_NAME = InfiniteAutoScrollWithCompositionalLayout; 576 | }; 577 | name = Release; 578 | }; 579 | /* End XCBuildConfiguration section */ 580 | 581 | /* Begin XCConfigurationList section */ 582 | D1300C53282E757100122349 /* Build configuration list for PBXProject "InfiniteAutoScrollWithCompositionalLayout" */ = { 583 | isa = XCConfigurationList; 584 | buildConfigurations = ( 585 | D1300C80282E757200122349 /* Debug */, 586 | D1300C81282E757200122349 /* Release */, 587 | ); 588 | defaultConfigurationIsVisible = 0; 589 | defaultConfigurationName = Release; 590 | }; 591 | D1300C82282E757200122349 /* Build configuration list for PBXNativeTarget "InfiniteAutoScrollWithCompositionalLayout" */ = { 592 | isa = XCConfigurationList; 593 | buildConfigurations = ( 594 | D1300C83282E757200122349 /* Debug */, 595 | D1300C84282E757200122349 /* Release */, 596 | ); 597 | defaultConfigurationIsVisible = 0; 598 | defaultConfigurationName = Release; 599 | }; 600 | D1300C85282E757200122349 /* Build configuration list for PBXNativeTarget "InfiniteAutoScrollWithCompositionalLayoutTests" */ = { 601 | isa = XCConfigurationList; 602 | buildConfigurations = ( 603 | D1300C86282E757200122349 /* Debug */, 604 | D1300C87282E757200122349 /* Release */, 605 | ); 606 | defaultConfigurationIsVisible = 0; 607 | defaultConfigurationName = Release; 608 | }; 609 | D1300C88282E757200122349 /* Build configuration list for PBXNativeTarget "InfiniteAutoScrollWithCompositionalLayoutUITests" */ = { 610 | isa = XCConfigurationList; 611 | buildConfigurations = ( 612 | D1300C89282E757200122349 /* Debug */, 613 | D1300C8A282E757200122349 /* Release */, 614 | ); 615 | defaultConfigurationIsVisible = 0; 616 | defaultConfigurationName = Release; 617 | }; 618 | /* End XCConfigurationList section */ 619 | }; 620 | rootObject = D1300C50282E757100122349 /* Project object */; 621 | } 622 | -------------------------------------------------------------------------------- /InfiniteAutoScrollWithCompositionalLayout.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /InfiniteAutoScrollWithCompositionalLayout.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /InfiniteAutoScrollWithCompositionalLayout/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // InfiniteAutoScrollWithCompositionalLayout 4 | // 5 | // Created by Rachel Chen on 2022/5/13. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | 14 | 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 16 | // Override point for customization after application launch. 17 | return true 18 | } 19 | 20 | // MARK: UISceneSession Lifecycle 21 | 22 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 23 | // Called when a new scene session is being created. 24 | // Use this method to select a configuration to create the new scene with. 25 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 26 | } 27 | 28 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 29 | // Called when the user discards a scene session. 30 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 31 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 32 | } 33 | 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /InfiniteAutoScrollWithCompositionalLayout/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /InfiniteAutoScrollWithCompositionalLayout/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rachelpeichen/Infinite_Auto_Scrolling_CollectionView_CompositionalLayout/8300284d8a7e403b3f27269d1151210ce7f6e6e1/InfiniteAutoScrollWithCompositionalLayout/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /InfiniteAutoScrollWithCompositionalLayout/Assets.xcassets/AppIcon.appiconset/120-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rachelpeichen/Infinite_Auto_Scrolling_CollectionView_CompositionalLayout/8300284d8a7e403b3f27269d1151210ce7f6e6e1/InfiniteAutoScrollWithCompositionalLayout/Assets.xcassets/AppIcon.appiconset/120-1.png -------------------------------------------------------------------------------- /InfiniteAutoScrollWithCompositionalLayout/Assets.xcassets/AppIcon.appiconset/120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rachelpeichen/Infinite_Auto_Scrolling_CollectionView_CompositionalLayout/8300284d8a7e403b3f27269d1151210ce7f6e6e1/InfiniteAutoScrollWithCompositionalLayout/Assets.xcassets/AppIcon.appiconset/120.png -------------------------------------------------------------------------------- /InfiniteAutoScrollWithCompositionalLayout/Assets.xcassets/AppIcon.appiconset/180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rachelpeichen/Infinite_Auto_Scrolling_CollectionView_CompositionalLayout/8300284d8a7e403b3f27269d1151210ce7f6e6e1/InfiniteAutoScrollWithCompositionalLayout/Assets.xcassets/AppIcon.appiconset/180.png -------------------------------------------------------------------------------- /InfiniteAutoScrollWithCompositionalLayout/Assets.xcassets/AppIcon.appiconset/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rachelpeichen/Infinite_Auto_Scrolling_CollectionView_CompositionalLayout/8300284d8a7e403b3f27269d1151210ce7f6e6e1/InfiniteAutoScrollWithCompositionalLayout/Assets.xcassets/AppIcon.appiconset/40.png -------------------------------------------------------------------------------- /InfiniteAutoScrollWithCompositionalLayout/Assets.xcassets/AppIcon.appiconset/58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rachelpeichen/Infinite_Auto_Scrolling_CollectionView_CompositionalLayout/8300284d8a7e403b3f27269d1151210ce7f6e6e1/InfiniteAutoScrollWithCompositionalLayout/Assets.xcassets/AppIcon.appiconset/58.png -------------------------------------------------------------------------------- /InfiniteAutoScrollWithCompositionalLayout/Assets.xcassets/AppIcon.appiconset/60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rachelpeichen/Infinite_Auto_Scrolling_CollectionView_CompositionalLayout/8300284d8a7e403b3f27269d1151210ce7f6e6e1/InfiniteAutoScrollWithCompositionalLayout/Assets.xcassets/AppIcon.appiconset/60.png -------------------------------------------------------------------------------- /InfiniteAutoScrollWithCompositionalLayout/Assets.xcassets/AppIcon.appiconset/80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rachelpeichen/Infinite_Auto_Scrolling_CollectionView_CompositionalLayout/8300284d8a7e403b3f27269d1151210ce7f6e6e1/InfiniteAutoScrollWithCompositionalLayout/Assets.xcassets/AppIcon.appiconset/80.png -------------------------------------------------------------------------------- /InfiniteAutoScrollWithCompositionalLayout/Assets.xcassets/AppIcon.appiconset/87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rachelpeichen/Infinite_Auto_Scrolling_CollectionView_CompositionalLayout/8300284d8a7e403b3f27269d1151210ce7f6e6e1/InfiniteAutoScrollWithCompositionalLayout/Assets.xcassets/AppIcon.appiconset/87.png -------------------------------------------------------------------------------- /InfiniteAutoScrollWithCompositionalLayout/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "40.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "60.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "58.png", 17 | "idiom" : "iphone", 18 | "scale" : "2x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "87.png", 23 | "idiom" : "iphone", 24 | "scale" : "3x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "80.png", 29 | "idiom" : "iphone", 30 | "scale" : "2x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "filename" : "120-1.png", 35 | "idiom" : "iphone", 36 | "scale" : "3x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "120.png", 41 | "idiom" : "iphone", 42 | "scale" : "2x", 43 | "size" : "60x60" 44 | }, 45 | { 46 | "filename" : "180.png", 47 | "idiom" : "iphone", 48 | "scale" : "3x", 49 | "size" : "60x60" 50 | }, 51 | { 52 | "idiom" : "ipad", 53 | "scale" : "1x", 54 | "size" : "20x20" 55 | }, 56 | { 57 | "idiom" : "ipad", 58 | "scale" : "2x", 59 | "size" : "20x20" 60 | }, 61 | { 62 | "idiom" : "ipad", 63 | "scale" : "1x", 64 | "size" : "29x29" 65 | }, 66 | { 67 | "idiom" : "ipad", 68 | "scale" : "2x", 69 | "size" : "29x29" 70 | }, 71 | { 72 | "idiom" : "ipad", 73 | "scale" : "1x", 74 | "size" : "40x40" 75 | }, 76 | { 77 | "idiom" : "ipad", 78 | "scale" : "2x", 79 | "size" : "40x40" 80 | }, 81 | { 82 | "idiom" : "ipad", 83 | "scale" : "1x", 84 | "size" : "76x76" 85 | }, 86 | { 87 | "idiom" : "ipad", 88 | "scale" : "2x", 89 | "size" : "76x76" 90 | }, 91 | { 92 | "idiom" : "ipad", 93 | "scale" : "2x", 94 | "size" : "83.5x83.5" 95 | }, 96 | { 97 | "filename" : "1024.png", 98 | "idiom" : "ios-marketing", 99 | "scale" : "1x", 100 | "size" : "1024x1024" 101 | } 102 | ], 103 | "info" : { 104 | "author" : "xcode", 105 | "version" : 1 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /InfiniteAutoScrollWithCompositionalLayout/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /InfiniteAutoScrollWithCompositionalLayout/Assets.xcassets/photo_1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "photo_1.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /InfiniteAutoScrollWithCompositionalLayout/Assets.xcassets/photo_1.imageset/photo_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rachelpeichen/Infinite_Auto_Scrolling_CollectionView_CompositionalLayout/8300284d8a7e403b3f27269d1151210ce7f6e6e1/InfiniteAutoScrollWithCompositionalLayout/Assets.xcassets/photo_1.imageset/photo_1.jpg -------------------------------------------------------------------------------- /InfiniteAutoScrollWithCompositionalLayout/Assets.xcassets/photo_2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "photo_2.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /InfiniteAutoScrollWithCompositionalLayout/Assets.xcassets/photo_2.imageset/photo_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rachelpeichen/Infinite_Auto_Scrolling_CollectionView_CompositionalLayout/8300284d8a7e403b3f27269d1151210ce7f6e6e1/InfiniteAutoScrollWithCompositionalLayout/Assets.xcassets/photo_2.imageset/photo_2.jpg -------------------------------------------------------------------------------- /InfiniteAutoScrollWithCompositionalLayout/Assets.xcassets/photo_3.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "photo_3.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /InfiniteAutoScrollWithCompositionalLayout/Assets.xcassets/photo_3.imageset/photo_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rachelpeichen/Infinite_Auto_Scrolling_CollectionView_CompositionalLayout/8300284d8a7e403b3f27269d1151210ce7f6e6e1/InfiniteAutoScrollWithCompositionalLayout/Assets.xcassets/photo_3.imageset/photo_3.jpg -------------------------------------------------------------------------------- /InfiniteAutoScrollWithCompositionalLayout/Assets.xcassets/photo_4.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "photo_4.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /InfiniteAutoScrollWithCompositionalLayout/Assets.xcassets/photo_4.imageset/photo_4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rachelpeichen/Infinite_Auto_Scrolling_CollectionView_CompositionalLayout/8300284d8a7e403b3f27269d1151210ce7f6e6e1/InfiniteAutoScrollWithCompositionalLayout/Assets.xcassets/photo_4.imageset/photo_4.jpg -------------------------------------------------------------------------------- /InfiniteAutoScrollWithCompositionalLayout/Assets.xcassets/photo_5.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "photo_5.jpg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /InfiniteAutoScrollWithCompositionalLayout/Assets.xcassets/photo_5.imageset/photo_5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rachelpeichen/Infinite_Auto_Scrolling_CollectionView_CompositionalLayout/8300284d8a7e403b3f27269d1151210ce7f6e6e1/InfiniteAutoScrollWithCompositionalLayout/Assets.xcassets/photo_5.imageset/photo_5.jpg -------------------------------------------------------------------------------- /InfiniteAutoScrollWithCompositionalLayout/Base.lproj/LaunchScreen.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 | -------------------------------------------------------------------------------- /InfiniteAutoScrollWithCompositionalLayout/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 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /InfiniteAutoScrollWithCompositionalLayout/InfiniteAutoScrollView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InfiniteAutoScrollView.swift 3 | // InfiniteAutoScrollWithCompositionalLayout 4 | // 5 | // Created by Rachel Chen on 2022/5/13. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol InfiniteAutoScrollViewDelegate: AnyObject { 11 | func didTapItem(_ collectionView: UICollectionView, selectedItem item:Int) 12 | } 13 | 14 | class InfiniteAutoScrollView: UIView { 15 | 16 | // MARK: - Properties 17 | weak var delegate: InfiniteAutoScrollViewDelegate? 18 | var collectionView: UICollectionView! 19 | var pageControl: UIPageControl! 20 | var currentFrame: CGRect! 21 | var autoScrollTimer: Timer! 22 | var currentAutoScrollIndex = 1 23 | 24 | var contentArray = [AnyObject]() { 25 | didSet { 26 | if contentArray.count > 1 { 27 | /// Modify it to be like [C, A, B, C, A] to make infinite effect 28 | contentArray.insert(contentArray.last!, at: 0) 29 | contentArray.append(contentArray[1]) 30 | } 31 | 32 | if collectionView != nil { 33 | collectionView.reloadData() 34 | collectionView.scrollToItem(at: IndexPath(item: 1, section: 0), at: .left, animated: false) 35 | addPageControl() 36 | } 37 | } 38 | } 39 | 40 | /// Default is false 41 | var isAutoScrollEnabled = false { 42 | didSet { 43 | if collectionView != nil && isAutoScrollEnabled == true { 44 | configAutoScroll() 45 | } 46 | } 47 | } 48 | 49 | /// Time interval for auto scroll 50 | var timeInterval = 1.0 { 51 | didSet { 52 | if collectionView != nil && isAutoScrollEnabled == true { 53 | configAutoScroll() 54 | } 55 | } 56 | } 57 | 58 | /// Default is true 59 | var isPageControlShown = true { 60 | didSet { 61 | if pageControl != nil && isPageControlShown == false { 62 | pageControl.isHidden = true 63 | } 64 | } 65 | } 66 | 67 | /// Current page color for UIPageControl 68 | var currentPageControlColor: UIColor? { 69 | didSet { 70 | if collectionView != nil && pageControl != nil { 71 | pageControl.currentPageIndicatorTintColor = currentPageControlColor 72 | } 73 | } 74 | } 75 | 76 | /// Other page color for UIPageControl 77 | var pageControlTintColor: UIColor? { 78 | didSet { 79 | if collectionView != nil && pageControl != nil { 80 | pageControl.pageIndicatorTintColor = pageControlTintColor 81 | } 82 | } 83 | } 84 | 85 | // MARK: - Initializers 86 | override init(frame: CGRect) { 87 | super.init(frame: frame) 88 | initCollectionView() 89 | } 90 | 91 | required init?(coder aDecoder: NSCoder) { 92 | super.init(coder: aDecoder) 93 | initCollectionView() 94 | } 95 | 96 | // MARK: - Custom UI Layout Methods 97 | func initCollectionView() { 98 | collectionView = UICollectionView(frame: .zero, collectionViewLayout: createCompositionalLayout()) 99 | collectionView.dataSource = self 100 | collectionView.delegate = self 101 | collectionView.register(InfiniteAutoScrollViewCell.self, forCellWithReuseIdentifier: "InfiniteAutoScrollViewCell") 102 | collectionView.showsHorizontalScrollIndicator = false 103 | collectionView.translatesAutoresizingMaskIntoConstraints = false 104 | addSubview(collectionView) 105 | 106 | NSLayoutConstraint.activate([ 107 | collectionView.topAnchor.constraint(equalTo: self.topAnchor), 108 | collectionView.leadingAnchor.constraint(equalTo: self.leadingAnchor), 109 | collectionView.trailingAnchor.constraint(equalTo: self.trailingAnchor), 110 | collectionView.bottomAnchor.constraint(equalTo: self.bottomAnchor) 111 | ]) 112 | 113 | if isPageControlShown { 114 | addPageControl() 115 | } 116 | } 117 | 118 | func addPageControl() { 119 | pageControl = UIPageControl(frame: CGRect(x: self.frame.origin.x, 120 | y: self.collectionView.frame.origin.y + self.frame.height, 121 | width: self.frame.size.width, 122 | height: 40.0)) 123 | pageControl.numberOfPages = contentArray.count - 2 124 | pageControl.currentPageIndicatorTintColor = currentPageControlColor 125 | pageControl.pageIndicatorTintColor = pageControlTintColor 126 | pageControl.addTarget(self, action: #selector(changePage(_:)), for: .valueChanged) 127 | addSubview(pageControl) 128 | } 129 | 130 | @objc func changePage(_ sender: UIPageControl) { 131 | collectionView.scrollToItem(at: IndexPath(item: sender.currentPage + 1, section: 0), at: .left, animated: true) 132 | } 133 | 134 | func createCompositionalLayout() -> UICollectionViewLayout { 135 | UICollectionViewCompositionalLayout { sectionIndex, layoutEnvironment in 136 | return self.createHorizontalScrollLayoutSection() 137 | } 138 | } 139 | 140 | func createHorizontalScrollLayoutSection() -> NSCollectionLayoutSection { 141 | let itemInset = 5.0 142 | let sectionMargin = 15.0 143 | 144 | // Item 145 | let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1)) 146 | let layoutItem = NSCollectionLayoutItem(layoutSize: itemSize) 147 | 148 | layoutItem.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: itemInset, bottom: 0, trailing: itemInset) 149 | 150 | // Group 151 | let pageWidth = collectionView.bounds.width - sectionMargin * 2 152 | print("pageWidth = \(collectionView.bounds.width)") 153 | let layoutGroupSize = NSCollectionLayoutSize(widthDimension: .absolute(CGFloat(pageWidth)), heightDimension: .estimated(self.frame.height)) 154 | print("self.frame.height = \(self.frame.height)") 155 | let layoutGroup = NSCollectionLayoutGroup.horizontal(layoutSize: layoutGroupSize, subitems: [layoutItem]) 156 | 157 | // Section 158 | let layoutSection = NSCollectionLayoutSection(group: layoutGroup) 159 | layoutSection.orthogonalScrollingBehavior = .groupPagingCentered 160 | 161 | /// When we use orthogonalScrollingBehavior, scrollViewDidScroll(_:) and scrollViewDidEndDecelerating(_:) won't be fired 162 | /// visibleItemsInvalidationHandler will be fired when user scroll 163 | layoutSection.visibleItemsInvalidationHandler = { visibleItems, point, environment in 164 | if var page = Int(exactly: (point.x + sectionMargin) / pageWidth) { 165 | let maxIndex = self.contentArray.indices.max()! 166 | self.currentAutoScrollIndex = page 167 | 168 | /// Setup for infinite scroll; we had modify the data array to be [C, A, B, C, A] 169 | if page == maxIndex { 170 | /// When at last item, need to change to array[1], so it can continue to scroll right or left 171 | page = 1 172 | self.currentAutoScrollIndex = page 173 | } else if page == 0 { 174 | /// When at fist item, need to change to array[3], so it can continue to scroll right or left 175 | page = maxIndex - 1 176 | self.currentAutoScrollIndex = page 177 | } 178 | 179 | /// Because we add a data in array 180 | let realPage = page - 1 181 | 182 | /// Update page control and cell only when page changed 183 | if self.pageControl.currentPage != realPage { 184 | self.pageControl.currentPage = realPage 185 | self.collectionView.scrollToItem(at: IndexPath(item: page, section: 0), at: .left, animated: false) 186 | } 187 | 188 | if self.isAutoScrollEnabled { 189 | self.configAutoScroll() 190 | } 191 | } 192 | } 193 | 194 | return layoutSection 195 | } 196 | } 197 | 198 | // MARK: - Auto Scroll Methods 199 | extension InfiniteAutoScrollView { 200 | 201 | func configAutoScroll() { 202 | resetAutoScrollTimer() 203 | if contentArray.count > 1 { 204 | setupAutoScrollTimer() 205 | } 206 | } 207 | 208 | func resetAutoScrollTimer() { 209 | if autoScrollTimer != nil { 210 | autoScrollTimer.invalidate() 211 | autoScrollTimer = nil 212 | } 213 | } 214 | 215 | func setupAutoScrollTimer() { 216 | autoScrollTimer = Timer.scheduledTimer(timeInterval: timeInterval, target: self, selector: #selector(autoScrollAction(timer:)), userInfo: nil, repeats: true) 217 | RunLoop.main.add(autoScrollTimer, forMode: RunLoop.Mode.common) 218 | } 219 | 220 | @objc func autoScrollAction(timer: Timer) { 221 | // if self.window != nil { 222 | currentAutoScrollIndex += 1 223 | if currentAutoScrollIndex >= contentArray.count { 224 | currentAutoScrollIndex = currentAutoScrollIndex % contentArray.count 225 | } 226 | collectionView.scrollToItem(at: IndexPath(item: currentAutoScrollIndex, section: 0), at: .left, animated: true) 227 | // } 228 | } 229 | } 230 | 231 | // MARK: - InfiniteAutoScrollViewCellDelegate 232 | extension InfiniteAutoScrollView: InfiniteAutoScrollViewCellDelegate { 233 | 234 | func invalidateTimer() { 235 | if autoScrollTimer != nil { 236 | autoScrollTimer.invalidate() 237 | autoScrollTimer = nil 238 | } 239 | } 240 | } 241 | 242 | // MARK: - UICollectionViewDataSource 243 | extension InfiniteAutoScrollView: UICollectionViewDataSource { 244 | 245 | func numberOfSections(in collectionView: UICollectionView) -> Int { 246 | return 1 247 | } 248 | 249 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 250 | return contentArray.count 251 | } 252 | 253 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 254 | guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "InfiniteAutoScrollViewCell", for: indexPath) as? InfiniteAutoScrollViewCell else { 255 | return UICollectionViewCell() 256 | } 257 | 258 | let content = contentArray[indexPath.item] 259 | 260 | if let realContent = content as? UIImage { 261 | cell.imageView.image = realContent 262 | } 263 | 264 | cell.delegate = self 265 | 266 | return cell 267 | } 268 | } 269 | 270 | 271 | // MARK: - UICollectionViewDelegate 272 | extension InfiniteAutoScrollView: UICollectionViewDelegate { 273 | 274 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 275 | delegate?.didTapItem(collectionView, selectedItem: indexPath.item) 276 | } 277 | } 278 | 279 | -------------------------------------------------------------------------------- /InfiniteAutoScrollWithCompositionalLayout/InfiniteAutoScrollViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InfiniteAutoScrollViewCell.swift 3 | // InfiniteAutoScrollWithCompositionalLayout 4 | // 5 | // Created by Rachel Chen on 2022/5/13. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol InfiniteAutoScrollViewCellDelegate: AnyObject { 11 | func invalidateTimer() 12 | } 13 | 14 | class InfiniteAutoScrollViewCell: UICollectionViewCell { 15 | 16 | weak var delegate: InfiniteAutoScrollViewCellDelegate? 17 | 18 | lazy var imageView: UIImageView = { 19 | let imageView = UIImageView(frame: .zero) 20 | imageView.contentMode = .scaleAspectFill 21 | imageView.clipsToBounds = true 22 | imageView.layer.cornerRadius = 20 23 | return imageView 24 | }() 25 | 26 | // MARK: - Initializer 27 | override init(frame: CGRect) { 28 | super.init(frame: frame) 29 | initCell() 30 | } 31 | 32 | required init?(coder aDecoder: NSCoder) { 33 | fatalError("init(coder:) has not been implemented") 34 | } 35 | 36 | private func initCell() { 37 | addSubview(imageView) 38 | imageView.translatesAutoresizingMaskIntoConstraints = false 39 | NSLayoutConstraint.activate([ 40 | imageView.trailingAnchor.constraint(equalTo: self.trailingAnchor), 41 | imageView.leadingAnchor.constraint(equalTo: self.leadingAnchor), 42 | imageView.topAnchor.constraint(equalTo: self.topAnchor), 43 | imageView.bottomAnchor.constraint(equalTo: self.bottomAnchor) 44 | ]) 45 | 46 | let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:))) 47 | panGesture.delegate = self 48 | self.addGestureRecognizer(panGesture) 49 | } 50 | 51 | @objc private func handlePan(_ pan: UIPanGestureRecognizer) { 52 | // Invalidate timer when user pan on cell 53 | delegate?.invalidateTimer() 54 | } 55 | } 56 | 57 | // MARK: - UIGestureRecognizerDelegate 58 | extension InfiniteAutoScrollViewCell: UIGestureRecognizerDelegate { 59 | 60 | func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { 61 | return true 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /InfiniteAutoScrollWithCompositionalLayout/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIApplicationSceneManifest 6 | 7 | UIApplicationSupportsMultipleScenes 8 | 9 | UISceneConfigurations 10 | 11 | UIWindowSceneSessionRoleApplication 12 | 13 | 14 | UISceneConfigurationName 15 | Default Configuration 16 | UISceneDelegateClassName 17 | $(PRODUCT_MODULE_NAME).SceneDelegate 18 | UISceneStoryboardFile 19 | Main 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /InfiniteAutoScrollWithCompositionalLayout/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // InfiniteAutoScrollWithCompositionalLayout 4 | // 5 | // Created by Rachel Chen on 2022/5/13. 6 | // 7 | 8 | import UIKit 9 | 10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | 12 | var window: UIWindow? 13 | 14 | 15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 16 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 17 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 18 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 19 | guard let _ = (scene as? UIWindowScene) else { return } 20 | } 21 | 22 | func sceneDidDisconnect(_ scene: UIScene) { 23 | // Called as the scene is being released by the system. 24 | // This occurs shortly after the scene enters the background, or when its session is discarded. 25 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 26 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). 27 | } 28 | 29 | func sceneDidBecomeActive(_ scene: UIScene) { 30 | // Called when the scene has moved from an inactive state to an active state. 31 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 32 | } 33 | 34 | func sceneWillResignActive(_ scene: UIScene) { 35 | // Called when the scene will move from an active state to an inactive state. 36 | // This may occur due to temporary interruptions (ex. an incoming phone call). 37 | } 38 | 39 | func sceneWillEnterForeground(_ scene: UIScene) { 40 | // Called as the scene transitions from the background to the foreground. 41 | // Use this method to undo the changes made on entering the background. 42 | } 43 | 44 | func sceneDidEnterBackground(_ scene: UIScene) { 45 | // Called as the scene transitions from the foreground to the background. 46 | // Use this method to save data, release shared resources, and store enough scene-specific state information 47 | // to restore the scene back to its current state. 48 | } 49 | 50 | 51 | } 52 | 53 | -------------------------------------------------------------------------------- /InfiniteAutoScrollWithCompositionalLayout/UIDevice+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIDevice+Extension.swift 3 | // InfiniteAutoScrollWithCompositionalLayout 4 | // 5 | // Created by Rachel Chen on 2022/5/17. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | public extension UIDevice { 12 | 13 | static let modelName: String = { 14 | var systemInfo = utsname() 15 | uname(&systemInfo) 16 | let machineMirror = Mirror(reflecting: systemInfo.machine) 17 | let identifier = machineMirror.children.reduce("") { identifier, element in 18 | guard let value = element.value as? Int8, value != 0 else { return identifier } 19 | return identifier + String(UnicodeScalar(UInt8(value))) 20 | } 21 | 22 | func mapToDevice(identifier: String) -> String { 23 | #if os(iOS) 24 | switch identifier { 25 | case "iPod5,1": return "iPod touch (5th generation)" 26 | case "iPod7,1": return "iPod touch (6th generation)" 27 | case "iPod9,1": return "iPod touch (7th generation)" 28 | case "iPhone3,1", "iPhone3,2", "iPhone3,3": return "iPhone 4" 29 | case "iPhone4,1": return "iPhone 4s" 30 | case "iPhone5,1", "iPhone5,2": return "iPhone 5" 31 | case "iPhone5,3", "iPhone5,4": return "iPhone 5c" 32 | case "iPhone6,1", "iPhone6,2": return "iPhone 5s" 33 | case "iPhone7,2": return "iPhone 6" 34 | case "iPhone7,1": return "iPhone 6 Plus" 35 | case "iPhone8,1": return "iPhone 6s" 36 | case "iPhone8,2": return "iPhone 6s Plus" 37 | case "iPhone9,1", "iPhone9,3": return "iPhone 7" 38 | case "iPhone9,2", "iPhone9,4": return "iPhone 7 Plus" 39 | case "iPhone10,1", "iPhone10,4": return "iPhone 8" 40 | case "iPhone10,2", "iPhone10,5": return "iPhone 8 Plus" 41 | case "iPhone10,3", "iPhone10,6": return "iPhone X" 42 | case "iPhone11,2": return "iPhone XS" 43 | case "iPhone11,4", "iPhone11,6": return "iPhone XS Max" 44 | case "iPhone11,8": return "iPhone XR" 45 | case "iPhone12,1": return "iPhone 11" 46 | case "iPhone12,3": return "iPhone 11 Pro" 47 | case "iPhone12,5": return "iPhone 11 Pro Max" 48 | case "iPhone13,1": return "iPhone 12 mini" 49 | case "iPhone13,2": return "iPhone 12" 50 | case "iPhone13,3": return "iPhone 12 Pro" 51 | case "iPhone13,4": return "iPhone 12 Pro Max" 52 | case "iPhone14,4": return "iPhone 13 mini" 53 | case "iPhone14,5": return "iPhone 13" 54 | case "iPhone14,2": return "iPhone 13 Pro" 55 | case "iPhone14,3": return "iPhone 13 Pro Max" 56 | case "iPhone8,4": return "iPhone SE" 57 | case "iPhone12,8": return "iPhone SE (2nd generation)" 58 | case "iPhone14,6": return "iPhone SE (3rd generation)" 59 | case "iPad2,1", "iPad2,2", "iPad2,3", "iPad2,4": return "iPad 2" 60 | case "iPad3,1", "iPad3,2", "iPad3,3": return "iPad (3rd generation)" 61 | case "iPad3,4", "iPad3,5", "iPad3,6": return "iPad (4th generation)" 62 | case "iPad6,11", "iPad6,12": return "iPad (5th generation)" 63 | case "iPad7,5", "iPad7,6": return "iPad (6th generation)" 64 | case "iPad7,11", "iPad7,12": return "iPad (7th generation)" 65 | case "iPad11,6", "iPad11,7": return "iPad (8th generation)" 66 | case "iPad12,1", "iPad12,2": return "iPad (9th generation)" 67 | case "iPad4,1", "iPad4,2", "iPad4,3": return "iPad Air" 68 | case "iPad5,3", "iPad5,4": return "iPad Air 2" 69 | case "iPad11,3", "iPad11,4": return "iPad Air (3rd generation)" 70 | case "iPad13,1", "iPad13,2": return "iPad Air (4th generation)" 71 | case "iPad13,16", "iPad13,17": return "iPad Air (5th generation)" 72 | case "iPad2,5", "iPad2,6", "iPad2,7": return "iPad mini" 73 | case "iPad4,4", "iPad4,5", "iPad4,6": return "iPad mini 2" 74 | case "iPad4,7", "iPad4,8", "iPad4,9": return "iPad mini 3" 75 | case "iPad5,1", "iPad5,2": return "iPad mini 4" 76 | case "iPad11,1", "iPad11,2": return "iPad mini (5th generation)" 77 | case "iPad14,1", "iPad14,2": return "iPad mini (6th generation)" 78 | case "iPad6,3", "iPad6,4": return "iPad Pro (9.7-inch)" 79 | case "iPad7,3", "iPad7,4": return "iPad Pro (10.5-inch)" 80 | case "iPad8,1", "iPad8,2", "iPad8,3", "iPad8,4": return "iPad Pro (11-inch) (1st generation)" 81 | case "iPad8,9", "iPad8,10": return "iPad Pro (11-inch) (2nd generation)" 82 | case "iPad13,4", "iPad13,5", "iPad13,6", "iPad13,7": return "iPad Pro (11-inch) (3rd generation)" 83 | case "iPad6,7", "iPad6,8": return "iPad Pro (12.9-inch) (1st generation)" 84 | case "iPad7,1", "iPad7,2": return "iPad Pro (12.9-inch) (2nd generation)" 85 | case "iPad8,5", "iPad8,6", "iPad8,7", "iPad8,8": return "iPad Pro (12.9-inch) (3rd generation)" 86 | case "iPad8,11", "iPad8,12": return "iPad Pro (12.9-inch) (4th generation)" 87 | case "iPad13,8", "iPad13,9", "iPad13,10", "iPad13,11":return "iPad Pro (12.9-inch) (5th generation)" 88 | case "AppleTV5,3": return "Apple TV" 89 | case "AppleTV6,2": return "Apple TV 4K" 90 | case "AudioAccessory1,1": return "HomePod" 91 | case "AudioAccessory5,1": return "HomePod mini" 92 | case "i386", "x86_64", "arm64": return "Simulator \(mapToDevice(identifier: ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"] ?? "iOS"))" 93 | default: return identifier 94 | } 95 | #elseif os(tvOS) 96 | switch identifier { 97 | case "AppleTV5,3": return "Apple TV 4" 98 | case "AppleTV6,2": return "Apple TV 4K" 99 | case "i386", "x86_64": return "Simulator \(mapToDevice(identifier: ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"] ?? "tvOS"))" 100 | default: return identifier 101 | } 102 | #endif 103 | } 104 | return mapToDevice(identifier: identifier) 105 | }() 106 | } 107 | -------------------------------------------------------------------------------- /InfiniteAutoScrollWithCompositionalLayout/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // InfiniteAutoScrollWithCompositionalLayout 4 | // 5 | // Created by Rachel Chen on 2022/5/13. 6 | // 7 | 8 | import UIKit 9 | 10 | class ViewController: UIViewController { 11 | 12 | @IBOutlet weak var demoView: InfiniteAutoScrollView! 13 | @IBOutlet weak var demoViewHeight: NSLayoutConstraint! 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | // Do any additional setup after loading the view. 18 | print("DemoView Width \(demoView.frame.width) in viewDidLoad") 19 | print("DemoView Height \(demoViewHeight.constant) in viewDidLoad") 20 | } 21 | 22 | override func viewDidAppear(_ animated: Bool) { 23 | super.viewDidAppear(animated) 24 | 25 | var dataArray = [UIImage?]() 26 | for i in 0..<5 { 27 | dataArray.append(UIImage(named: "photo_\(i+1)") ?? nil) 28 | } 29 | 30 | // Create view by storyboard 31 | demoView.contentArray = dataArray as [AnyObject] 32 | demoView.isAutoScrollEnabled = true 33 | demoView.timeInterval = 2.0 34 | demoView.isPageControlShown = true 35 | demoView.currentPageControlColor = .orange 36 | demoView.pageControlTintColor = .darkGray 37 | demoView.delegate = self 38 | demoViewHeight.constant = CGFloat(getPreferBannerViewHeightBasedOnDevice()) 39 | 40 | print("DemoView Width \(demoView.frame.width) in viewDidAppear") 41 | print("DemoView Height \(demoViewHeight.constant) in viewDidAppear") 42 | } 43 | 44 | func getPreferBannerViewHeightBasedOnDevice() -> Int { 45 | let modelName = UIDevice.modelName 46 | switch modelName { 47 | case "iPhone 5", "iPhone 5c", "iPhone 5s", "iPhone SE": 48 | return 158 49 | 50 | case "iPhone 6", "iPhone 6s", "iPhone 7", "iPhone 8", "iPhone SE (2nd generation)", "iPhone SE (3rd generation)": 51 | return 188 52 | 53 | case "iPhone 12", "Simulator iPhone 12", 54 | "iPhone 12 Pro", "Simulator iPhone 12 Pro", 55 | "iPhone 13", "Simulator iPhone 13", 56 | "iPhone 13 Pro", "Simulator iPhone 13 Pro": 57 | return 197 58 | 59 | case "iPhone 12 Pro Max", "Simulator iPhone 12 Pro Max", 60 | "iPhone 13 Pro Max", "Simulator iPhone 13 Pro Max": 61 | return 218 62 | 63 | default: 64 | return 210 65 | } 66 | } 67 | } 68 | 69 | // MARK: - InfiniteAutoScrollViewDelegate 70 | extension ViewController: InfiniteAutoScrollViewDelegate { 71 | 72 | func didTapItem(_ collectionView: UICollectionView, selectedItem item: Int) { 73 | if collectionView == demoView.collectionView { 74 | print("🥑 🥑 DemoView Item \(item) is tapped") 75 | } else { 76 | print("🥑 Other \(item) is tapped") 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /InfiniteAutoScrollWithCompositionalLayoutTests/InfiniteAutoScrollWithCompositionalLayoutTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InfiniteAutoScrollWithCompositionalLayoutTests.swift 3 | // InfiniteAutoScrollWithCompositionalLayoutTests 4 | // 5 | // Created by Rachel Chen on 2022/5/13. 6 | // 7 | 8 | import XCTest 9 | @testable import InfiniteAutoScrollWithCompositionalLayout 10 | 11 | class InfiniteAutoScrollWithCompositionalLayoutTests: XCTestCase { 12 | 13 | override func setUpWithError() throws { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | override func tearDownWithError() throws { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | func testExample() throws { 22 | // This is an example of a functional test case. 23 | // Use XCTAssert and related functions to verify your tests produce the correct results. 24 | // Any test you write for XCTest can be annotated as throws and async. 25 | // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. 26 | // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. 27 | } 28 | 29 | func testPerformanceExample() throws { 30 | // This is an example of a performance test case. 31 | self.measure { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /InfiniteAutoScrollWithCompositionalLayoutUITests/InfiniteAutoScrollWithCompositionalLayoutUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InfiniteAutoScrollWithCompositionalLayoutUITests.swift 3 | // InfiniteAutoScrollWithCompositionalLayoutUITests 4 | // 5 | // Created by Rachel Chen on 2022/5/13. 6 | // 7 | 8 | import XCTest 9 | 10 | class InfiniteAutoScrollWithCompositionalLayoutUITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | func testExample() throws { 26 | // UI tests must launch the application that they test. 27 | let app = XCUIApplication() 28 | app.launch() 29 | 30 | // Use XCTAssert and related functions to verify your tests produce the correct results. 31 | } 32 | 33 | func testLaunchPerformance() throws { 34 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { 35 | // This measures how long it takes to launch your application. 36 | measure(metrics: [XCTApplicationLaunchMetric()]) { 37 | XCUIApplication().launch() 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /InfiniteAutoScrollWithCompositionalLayoutUITests/InfiniteAutoScrollWithCompositionalLayoutUITestsLaunchTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InfiniteAutoScrollWithCompositionalLayoutUITestsLaunchTests.swift 3 | // InfiniteAutoScrollWithCompositionalLayoutUITests 4 | // 5 | // Created by Rachel Chen on 2022/5/13. 6 | // 7 | 8 | import XCTest 9 | 10 | class InfiniteAutoScrollWithCompositionalLayoutUITestsLaunchTests: XCTestCase { 11 | 12 | override class var runsForEachTargetApplicationUIConfiguration: Bool { 13 | true 14 | } 15 | 16 | override func setUpWithError() throws { 17 | continueAfterFailure = false 18 | } 19 | 20 | func testLaunch() throws { 21 | let app = XCUIApplication() 22 | app.launch() 23 | 24 | // Insert steps here to perform after app launch but before taking a screenshot, 25 | // such as logging into a test account or navigating somewhere in the app 26 | 27 | let attachment = XCTAttachment(screenshot: app.screenshot()) 28 | attachment.name = "Launch Screen" 29 | attachment.lifetime = .keepAlways 30 | add(attachment) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Rachel Chen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Infinite-Auto-Scroll Collection View by Compositional Layout 2 | #### This is a feature demo project inspired by the UI design requirement I encountered at work and also a possible solution that [I answered to this stackoverflow problem](https://stackoverflow.com/questions/69189323/how-to-auto-scroll-with-compositional-collection-view/72269055#72269055). 3 | 4 | ![This is an gif](https://github.com/rachelpeichen/rachelpeichen-infiniteAutoScrollCompositionalLayout/blob/main/Demo.gif) 5 | 6 | ## Features 7 | 8 | - The banner can scroll to next item automatically or manually 9 | - User can swipe to next item even when auto-scrolling enabled 10 | - The banner is displayed in infinite loop effect 11 | - Use **`UICollectionViewCompositionalLayout`** for building layout simply 12 | - Can be easily set up on storyboard 13 | - Page control, auto-scrolling, and scrolling time interval can be customized 14 | 15 | ## Development Environment 16 | 17 | * Swift 5 18 | * iOS 15.4 19 | * Xcode 13.3.1 20 | * macOS Monterey version 12.3 21 | 22 | ## Getting Started 23 | Clone repo or download `.zip` files and open `.xcodeproj` file then you can build on your computer 😊 24 | 25 | ## Acknowledgments 26 | Credit Photo by [JOSHUA COLEMAN](https://unsplash.com/@joshstyle?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/backgrounds/colors?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) 27 | 28 | ## Contact 29 | Rachel Chen rachel.hp.chen@gmail.com 30 | --------------------------------------------------------------------------------