├── .gitignore ├── InfinityScrollView.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── InfinityScrollView_Example.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── InfinityScrollView_Example ├── AppDelegate.swift ├── Resources │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Info.plist │ └── UI │ │ └── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard └── UI │ └── ExampleInfinityScrollViewController.swift ├── InfinityScrollView_Framework.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── InfinityScrollView_Framework └── InfinityScrollView_Framework.h ├── LICENSE.md ├── Podfile ├── Podfile.lock ├── README.md ├── Resources ├── infinity_scroll.gif ├── infinity_scroll_with_different_sizes.gif ├── infinity_scroll_with_snap_to_center.gif ├── single_item_behaviour.gif └── title_image.png ├── Shakuro.InfinityScrollView.podspec └── Source ├── InfinityScrollView+Models.swift ├── InfinityScrollView.swift └── SingleTouchDownGestureRecognizer.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | */build/* 3 | *.pbxuser 4 | !default.pbxuser 5 | *.mode1v3 6 | !default.mode1v3 7 | *.mode2v3 8 | !default.mode2v3 9 | *.perspectivev3 10 | !default.perspectivev3 11 | xcuserdata 12 | .DS_Store 13 | *.moved-aside 14 | DerivedData 15 | .idea/ 16 | *.hmap 17 | *.xccheckout 18 | 19 | ## Playgrounds 20 | timeline.xctimeline 21 | playground.xcworkspace 22 | 23 | # R.Swift 24 | *.generated.swift 25 | 26 | #CocoaPods 27 | #Pods 28 | -------------------------------------------------------------------------------- /InfinityScrollView.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /InfinityScrollView.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /InfinityScrollView_Example.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | A5B1D79D279E682600263D7A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5B1D79C279E682600263D7A /* AppDelegate.swift */; }; 11 | A5B1D7A4279E682600263D7A /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A5B1D7A2279E682600263D7A /* Main.storyboard */; }; 12 | A5B1D7A6279E682800263D7A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B1D7A5279E682800263D7A /* Assets.xcassets */; }; 13 | A5B1D7A9279E682800263D7A /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A5B1D7A7279E682800263D7A /* LaunchScreen.storyboard */; }; 14 | A5B1D7BD279E6B3100263D7A /* ExampleInfinityScrollViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5B1D7BC279E6B3100263D7A /* ExampleInfinityScrollViewController.swift */; }; 15 | A5B1D7C2279E6D6700263D7A /* InfinityScrollView_Framework.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5B1D7C1279E6D6700263D7A /* InfinityScrollView_Framework.framework */; }; 16 | A5B1D7C3279E6D6700263D7A /* InfinityScrollView_Framework.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A5B1D7C1279E6D6700263D7A /* InfinityScrollView_Framework.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 17 | /* End PBXBuildFile section */ 18 | 19 | /* Begin PBXCopyFilesBuildPhase section */ 20 | A5B1D7C4279E6D6700263D7A /* Embed Frameworks */ = { 21 | isa = PBXCopyFilesBuildPhase; 22 | buildActionMask = 2147483647; 23 | dstPath = ""; 24 | dstSubfolderSpec = 10; 25 | files = ( 26 | A5B1D7C3279E6D6700263D7A /* InfinityScrollView_Framework.framework in Embed Frameworks */, 27 | ); 28 | name = "Embed Frameworks"; 29 | runOnlyForDeploymentPostprocessing = 0; 30 | }; 31 | /* End PBXCopyFilesBuildPhase section */ 32 | 33 | /* Begin PBXFileReference section */ 34 | A5B1D799279E682600263D7A /* InfinityScrollView_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = InfinityScrollView_Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 35 | A5B1D79C279E682600263D7A /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 36 | A5B1D7A3279E682600263D7A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 37 | A5B1D7A5279E682800263D7A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 38 | A5B1D7A8279E682800263D7A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 39 | A5B1D7AA279E682800263D7A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 40 | A5B1D7BC279E6B3100263D7A /* ExampleInfinityScrollViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExampleInfinityScrollViewController.swift; sourceTree = ""; }; 41 | A5B1D7C1279E6D6700263D7A /* InfinityScrollView_Framework.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = InfinityScrollView_Framework.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 42 | /* End PBXFileReference section */ 43 | 44 | /* Begin PBXFrameworksBuildPhase section */ 45 | A5B1D796279E682600263D7A /* Frameworks */ = { 46 | isa = PBXFrameworksBuildPhase; 47 | buildActionMask = 2147483647; 48 | files = ( 49 | A5B1D7C2279E6D6700263D7A /* InfinityScrollView_Framework.framework in Frameworks */, 50 | ); 51 | runOnlyForDeploymentPostprocessing = 0; 52 | }; 53 | /* End PBXFrameworksBuildPhase section */ 54 | 55 | /* Begin PBXGroup section */ 56 | 237C45C2BD75CD4CEC26CFDC /* Pods */ = { 57 | isa = PBXGroup; 58 | children = ( 59 | ); 60 | path = Pods; 61 | sourceTree = ""; 62 | }; 63 | 6B9CC1B228D8692500DE744A /* Resources */ = { 64 | isa = PBXGroup; 65 | children = ( 66 | A5B1D7AA279E682800263D7A /* Info.plist */, 67 | A5B1D7A5279E682800263D7A /* Assets.xcassets */, 68 | 6B9CC1B328D8693100DE744A /* UI */, 69 | ); 70 | path = Resources; 71 | sourceTree = ""; 72 | }; 73 | 6B9CC1B328D8693100DE744A /* UI */ = { 74 | isa = PBXGroup; 75 | children = ( 76 | A5B1D7A7279E682800263D7A /* LaunchScreen.storyboard */, 77 | A5B1D7A2279E682600263D7A /* Main.storyboard */, 78 | ); 79 | path = UI; 80 | sourceTree = ""; 81 | }; 82 | 6B9CC1B428D8695C00DE744A /* UI */ = { 83 | isa = PBXGroup; 84 | children = ( 85 | A5B1D7BC279E6B3100263D7A /* ExampleInfinityScrollViewController.swift */, 86 | ); 87 | path = UI; 88 | sourceTree = ""; 89 | }; 90 | A5B1D790279E682500263D7A = { 91 | isa = PBXGroup; 92 | children = ( 93 | A5B1D79B279E682600263D7A /* InfinityScrollView_Example */, 94 | A5B1D79A279E682600263D7A /* Products */, 95 | A5B1D7C0279E6D6700263D7A /* Frameworks */, 96 | 237C45C2BD75CD4CEC26CFDC /* Pods */, 97 | ); 98 | sourceTree = ""; 99 | }; 100 | A5B1D79A279E682600263D7A /* Products */ = { 101 | isa = PBXGroup; 102 | children = ( 103 | A5B1D799279E682600263D7A /* InfinityScrollView_Example.app */, 104 | ); 105 | name = Products; 106 | sourceTree = ""; 107 | }; 108 | A5B1D79B279E682600263D7A /* InfinityScrollView_Example */ = { 109 | isa = PBXGroup; 110 | children = ( 111 | A5B1D79C279E682600263D7A /* AppDelegate.swift */, 112 | 6B9CC1B228D8692500DE744A /* Resources */, 113 | 6B9CC1B428D8695C00DE744A /* UI */, 114 | ); 115 | path = InfinityScrollView_Example; 116 | sourceTree = ""; 117 | }; 118 | A5B1D7C0279E6D6700263D7A /* Frameworks */ = { 119 | isa = PBXGroup; 120 | children = ( 121 | A5B1D7C1279E6D6700263D7A /* InfinityScrollView_Framework.framework */, 122 | ); 123 | name = Frameworks; 124 | sourceTree = ""; 125 | }; 126 | /* End PBXGroup section */ 127 | 128 | /* Begin PBXNativeTarget section */ 129 | A5B1D798279E682600263D7A /* InfinityScrollView_Example */ = { 130 | isa = PBXNativeTarget; 131 | buildConfigurationList = A5B1D7AD279E682800263D7A /* Build configuration list for PBXNativeTarget "InfinityScrollView_Example" */; 132 | buildPhases = ( 133 | A5B1D795279E682600263D7A /* Sources */, 134 | A5B1D796279E682600263D7A /* Frameworks */, 135 | A5B1D797279E682600263D7A /* Resources */, 136 | A5B1D7C4279E6D6700263D7A /* Embed Frameworks */, 137 | ); 138 | buildRules = ( 139 | ); 140 | dependencies = ( 141 | ); 142 | name = InfinityScrollView_Example; 143 | productName = InfinityScrollView_Example; 144 | productReference = A5B1D799279E682600263D7A /* InfinityScrollView_Example.app */; 145 | productType = "com.apple.product-type.application"; 146 | }; 147 | /* End PBXNativeTarget section */ 148 | 149 | /* Begin PBXProject section */ 150 | A5B1D791279E682500263D7A /* Project object */ = { 151 | isa = PBXProject; 152 | attributes = { 153 | BuildIndependentTargetsInParallel = 1; 154 | LastSwiftUpdateCheck = 1320; 155 | LastUpgradeCheck = 1320; 156 | TargetAttributes = { 157 | A5B1D798279E682600263D7A = { 158 | CreatedOnToolsVersion = 13.2.1; 159 | }; 160 | }; 161 | }; 162 | buildConfigurationList = A5B1D794279E682500263D7A /* Build configuration list for PBXProject "InfinityScrollView_Example" */; 163 | compatibilityVersion = "Xcode 13.0"; 164 | developmentRegion = en; 165 | hasScannedForEncodings = 0; 166 | knownRegions = ( 167 | en, 168 | Base, 169 | ); 170 | mainGroup = A5B1D790279E682500263D7A; 171 | productRefGroup = A5B1D79A279E682600263D7A /* Products */; 172 | projectDirPath = ""; 173 | projectRoot = ""; 174 | targets = ( 175 | A5B1D798279E682600263D7A /* InfinityScrollView_Example */, 176 | ); 177 | }; 178 | /* End PBXProject section */ 179 | 180 | /* Begin PBXResourcesBuildPhase section */ 181 | A5B1D797279E682600263D7A /* Resources */ = { 182 | isa = PBXResourcesBuildPhase; 183 | buildActionMask = 2147483647; 184 | files = ( 185 | A5B1D7A9279E682800263D7A /* LaunchScreen.storyboard in Resources */, 186 | A5B1D7A6279E682800263D7A /* Assets.xcassets in Resources */, 187 | A5B1D7A4279E682600263D7A /* Main.storyboard in Resources */, 188 | ); 189 | runOnlyForDeploymentPostprocessing = 0; 190 | }; 191 | /* End PBXResourcesBuildPhase section */ 192 | 193 | /* Begin PBXSourcesBuildPhase section */ 194 | A5B1D795279E682600263D7A /* Sources */ = { 195 | isa = PBXSourcesBuildPhase; 196 | buildActionMask = 2147483647; 197 | files = ( 198 | A5B1D7BD279E6B3100263D7A /* ExampleInfinityScrollViewController.swift in Sources */, 199 | A5B1D79D279E682600263D7A /* AppDelegate.swift in Sources */, 200 | ); 201 | runOnlyForDeploymentPostprocessing = 0; 202 | }; 203 | /* End PBXSourcesBuildPhase section */ 204 | 205 | /* Begin PBXVariantGroup section */ 206 | A5B1D7A2279E682600263D7A /* Main.storyboard */ = { 207 | isa = PBXVariantGroup; 208 | children = ( 209 | A5B1D7A3279E682600263D7A /* Base */, 210 | ); 211 | name = Main.storyboard; 212 | sourceTree = ""; 213 | }; 214 | A5B1D7A7279E682800263D7A /* LaunchScreen.storyboard */ = { 215 | isa = PBXVariantGroup; 216 | children = ( 217 | A5B1D7A8279E682800263D7A /* Base */, 218 | ); 219 | name = LaunchScreen.storyboard; 220 | sourceTree = ""; 221 | }; 222 | /* End PBXVariantGroup section */ 223 | 224 | /* Begin XCBuildConfiguration section */ 225 | A5B1D7AB279E682800263D7A /* Debug */ = { 226 | isa = XCBuildConfiguration; 227 | buildSettings = { 228 | ALWAYS_SEARCH_USER_PATHS = NO; 229 | CLANG_ANALYZER_NONNULL = YES; 230 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 231 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 232 | CLANG_CXX_LIBRARY = "libc++"; 233 | CLANG_ENABLE_MODULES = YES; 234 | CLANG_ENABLE_OBJC_ARC = YES; 235 | CLANG_ENABLE_OBJC_WEAK = YES; 236 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 237 | CLANG_WARN_BOOL_CONVERSION = YES; 238 | CLANG_WARN_COMMA = YES; 239 | CLANG_WARN_CONSTANT_CONVERSION = YES; 240 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 241 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 242 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 243 | CLANG_WARN_EMPTY_BODY = YES; 244 | CLANG_WARN_ENUM_CONVERSION = YES; 245 | CLANG_WARN_INFINITE_RECURSION = YES; 246 | CLANG_WARN_INT_CONVERSION = YES; 247 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 248 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 249 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 250 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 251 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 252 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 253 | CLANG_WARN_STRICT_PROTOTYPES = YES; 254 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 255 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 256 | CLANG_WARN_UNREACHABLE_CODE = YES; 257 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 258 | COPY_PHASE_STRIP = NO; 259 | DEBUG_INFORMATION_FORMAT = dwarf; 260 | ENABLE_STRICT_OBJC_MSGSEND = YES; 261 | ENABLE_TESTABILITY = YES; 262 | GCC_C_LANGUAGE_STANDARD = gnu11; 263 | GCC_DYNAMIC_NO_PIC = NO; 264 | GCC_NO_COMMON_BLOCKS = YES; 265 | GCC_OPTIMIZATION_LEVEL = 0; 266 | GCC_PREPROCESSOR_DEFINITIONS = ( 267 | "DEBUG=1", 268 | "$(inherited)", 269 | ); 270 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 271 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 272 | GCC_WARN_UNDECLARED_SELECTOR = YES; 273 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 274 | GCC_WARN_UNUSED_FUNCTION = YES; 275 | GCC_WARN_UNUSED_VARIABLE = YES; 276 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 277 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 278 | MTL_FAST_MATH = YES; 279 | ONLY_ACTIVE_ARCH = YES; 280 | SDKROOT = iphoneos; 281 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 282 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 283 | }; 284 | name = Debug; 285 | }; 286 | A5B1D7AC279E682800263D7A /* Release */ = { 287 | isa = XCBuildConfiguration; 288 | buildSettings = { 289 | ALWAYS_SEARCH_USER_PATHS = NO; 290 | CLANG_ANALYZER_NONNULL = YES; 291 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 292 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 293 | CLANG_CXX_LIBRARY = "libc++"; 294 | CLANG_ENABLE_MODULES = YES; 295 | CLANG_ENABLE_OBJC_ARC = YES; 296 | CLANG_ENABLE_OBJC_WEAK = YES; 297 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 298 | CLANG_WARN_BOOL_CONVERSION = YES; 299 | CLANG_WARN_COMMA = YES; 300 | CLANG_WARN_CONSTANT_CONVERSION = YES; 301 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 302 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 303 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 304 | CLANG_WARN_EMPTY_BODY = YES; 305 | CLANG_WARN_ENUM_CONVERSION = YES; 306 | CLANG_WARN_INFINITE_RECURSION = YES; 307 | CLANG_WARN_INT_CONVERSION = YES; 308 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 309 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 310 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 311 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 312 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 313 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 314 | CLANG_WARN_STRICT_PROTOTYPES = YES; 315 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 316 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 317 | CLANG_WARN_UNREACHABLE_CODE = YES; 318 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 319 | COPY_PHASE_STRIP = NO; 320 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 321 | ENABLE_NS_ASSERTIONS = NO; 322 | ENABLE_STRICT_OBJC_MSGSEND = YES; 323 | GCC_C_LANGUAGE_STANDARD = gnu11; 324 | GCC_NO_COMMON_BLOCKS = YES; 325 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 326 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 327 | GCC_WARN_UNDECLARED_SELECTOR = YES; 328 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 329 | GCC_WARN_UNUSED_FUNCTION = YES; 330 | GCC_WARN_UNUSED_VARIABLE = YES; 331 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 332 | MTL_ENABLE_DEBUG_INFO = NO; 333 | MTL_FAST_MATH = YES; 334 | SDKROOT = iphoneos; 335 | SWIFT_COMPILATION_MODE = wholemodule; 336 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 337 | VALIDATE_PRODUCT = YES; 338 | }; 339 | name = Release; 340 | }; 341 | A5B1D7AE279E682800263D7A /* Debug */ = { 342 | isa = XCBuildConfiguration; 343 | buildSettings = { 344 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 345 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 346 | CODE_SIGN_STYLE = Automatic; 347 | CURRENT_PROJECT_VERSION = 1; 348 | GENERATE_INFOPLIST_FILE = YES; 349 | INFOPLIST_FILE = InfinityScrollView_Example/Resources/Info.plist; 350 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 351 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 352 | INFOPLIST_KEY_UIMainStoryboardFile = Main; 353 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 354 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 355 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 356 | LD_RUNPATH_SEARCH_PATHS = ( 357 | "$(inherited)", 358 | "@executable_path/Frameworks", 359 | ); 360 | MARKETING_VERSION = 1.0; 361 | PRODUCT_BUNDLE_IDENTIFIER = "Shakuro.InfinityScrollView-Example"; 362 | PRODUCT_NAME = "$(TARGET_NAME)"; 363 | SWIFT_EMIT_LOC_STRINGS = YES; 364 | SWIFT_VERSION = 5.0; 365 | TARGETED_DEVICE_FAMILY = "1,2"; 366 | }; 367 | name = Debug; 368 | }; 369 | A5B1D7AF279E682800263D7A /* Release */ = { 370 | isa = XCBuildConfiguration; 371 | buildSettings = { 372 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 373 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 374 | CODE_SIGN_STYLE = Automatic; 375 | CURRENT_PROJECT_VERSION = 1; 376 | GENERATE_INFOPLIST_FILE = YES; 377 | INFOPLIST_FILE = InfinityScrollView_Example/Resources/Info.plist; 378 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 379 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 380 | INFOPLIST_KEY_UIMainStoryboardFile = Main; 381 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 382 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 383 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 384 | LD_RUNPATH_SEARCH_PATHS = ( 385 | "$(inherited)", 386 | "@executable_path/Frameworks", 387 | ); 388 | MARKETING_VERSION = 1.0; 389 | PRODUCT_BUNDLE_IDENTIFIER = "Shakuro.InfinityScrollView-Example"; 390 | PRODUCT_NAME = "$(TARGET_NAME)"; 391 | SWIFT_EMIT_LOC_STRINGS = YES; 392 | SWIFT_VERSION = 5.0; 393 | TARGETED_DEVICE_FAMILY = "1,2"; 394 | }; 395 | name = Release; 396 | }; 397 | /* End XCBuildConfiguration section */ 398 | 399 | /* Begin XCConfigurationList section */ 400 | A5B1D794279E682500263D7A /* Build configuration list for PBXProject "InfinityScrollView_Example" */ = { 401 | isa = XCConfigurationList; 402 | buildConfigurations = ( 403 | A5B1D7AB279E682800263D7A /* Debug */, 404 | A5B1D7AC279E682800263D7A /* Release */, 405 | ); 406 | defaultConfigurationIsVisible = 0; 407 | defaultConfigurationName = Release; 408 | }; 409 | A5B1D7AD279E682800263D7A /* Build configuration list for PBXNativeTarget "InfinityScrollView_Example" */ = { 410 | isa = XCConfigurationList; 411 | buildConfigurations = ( 412 | A5B1D7AE279E682800263D7A /* Debug */, 413 | A5B1D7AF279E682800263D7A /* Release */, 414 | ); 415 | defaultConfigurationIsVisible = 0; 416 | defaultConfigurationName = Release; 417 | }; 418 | /* End XCConfigurationList section */ 419 | }; 420 | rootObject = A5B1D791279E682500263D7A /* Project object */; 421 | } 422 | -------------------------------------------------------------------------------- /InfinityScrollView_Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /InfinityScrollView_Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /InfinityScrollView_Example/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Shakuro (https://shakuro.com/) 3 | // 4 | 5 | import UIKit 6 | 7 | @main 8 | class AppDelegate: UIResponder, UIApplicationDelegate { 9 | 10 | var window: UIWindow? 11 | 12 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 13 | // Override point for customization after application launch. 14 | return true 15 | } 16 | 17 | } 18 | 19 | -------------------------------------------------------------------------------- /InfinityScrollView_Example/Resources/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 | -------------------------------------------------------------------------------- /InfinityScrollView_Example/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /InfinityScrollView_Example/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /InfinityScrollView_Example/Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIMainStoryboardFile 6 | Main 7 | 8 | 9 | -------------------------------------------------------------------------------- /InfinityScrollView_Example/Resources/UI/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 | -------------------------------------------------------------------------------- /InfinityScrollView_Example/Resources/UI/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 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | -------------------------------------------------------------------------------- /InfinityScrollView_Example/UI/ExampleInfinityScrollViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Shakuro (https://shakuro.com/) 3 | // 4 | 5 | import InfinityScrollView_Framework 6 | import Shakuro_CommonTypes 7 | import UIKit 8 | 9 | class ExampleInfinityScrollViewController: UIViewController { 10 | 11 | @IBOutlet private var infinityScrollView: InfinityScrollView! 12 | @IBOutlet private var numberOfItemsTextField: UITextField! 13 | @IBOutlet private var numberOfItemsSegmentedControl: UISegmentedControl! 14 | @IBOutlet private var useConstantItemWidthSwitch: UISwitch! 15 | @IBOutlet private var constantItemWidthSwitch: UISwitch! 16 | @IBOutlet private var showItemsBackgroundSwitch: UISwitch! 17 | @IBOutlet private var showVisualCenterMarkerSwitch: UISwitch! 18 | @IBOutlet private var visualCenterMarkerView: UIView! 19 | @IBOutlet private var showContentCenterMarkerSwitch: UISwitch! 20 | @IBOutlet private var useHalfHeightSwitch: UISwitch! 21 | @IBOutlet private var isDecelerationEnabledSwitch: UISwitch! 22 | @IBOutlet private var useFastDecelerationRateSwitch: UISwitch! 23 | @IBOutlet private var isSnapEnabledSwitch: UISwitch! 24 | @IBOutlet private var snapAnimationSegmentedControl1: UISegmentedControl! 25 | @IBOutlet private var snapAnimationSegmentedControl2: UISegmentedControl! 26 | @IBOutlet private var singleItemBehaviourSegmentedControl: UISegmentedControl! 27 | 28 | private var contentCenterMarkerView: UIView! 29 | 30 | @IBOutlet private var infinityScrollViewBottomConstraint: NSLayoutConstraint! 31 | @IBOutlet private var infinityScrollViewHeightConstraint: NSLayoutConstraint! 32 | 33 | private var keyboardHandler: KeyboardHandler? 34 | private var numberOfItems: Int = 10 35 | 36 | // MARK: - Initialization 37 | 38 | override func viewDidLoad() { 39 | super.viewDidLoad() 40 | infinityScrollView.dataSource = self 41 | infinityScrollView.delegate = self 42 | infinityScrollView.snapAnimation = .scrollView 43 | infinityScrollView.setSingleItemBehavior(.tile) 44 | infinityScrollView.isSnapEnabled = true 45 | 46 | infinityScrollViewHeightConstraint.constant = 200 47 | numberOfItemsTextField.text = "\(numberOfItems)" 48 | numberOfItemsTextField.delegate = self 49 | numberOfItemsSegmentedControl.selectedSegmentIndex = 3 50 | useConstantItemWidthSwitch.isOn = false 51 | constantItemWidthSwitch.isOn = false 52 | showItemsBackgroundSwitch.isOn = false 53 | showVisualCenterMarkerSwitch.isOn = true 54 | visualCenterMarkerView.backgroundColor = UIColor.red 55 | showContentCenterMarkerSwitch.isOn = true 56 | useHalfHeightSwitch.isOn = false 57 | isDecelerationEnabledSwitch.isOn = infinityScrollView.isDecelerationEnabled 58 | useFastDecelerationRateSwitch.isOn = infinityScrollView.decelerationRate == .fast 59 | isSnapEnabledSwitch.isOn = infinityScrollView.isSnapEnabled 60 | snapAnimationSegmentedControl1.selectedSegmentIndex = 1 61 | snapAnimationSegmentedControl2.selectedSegmentIndex = -1 62 | singleItemBehaviourSegmentedControl.selectedSegmentIndex = 0 63 | // a little bit of hacks (do not do this, kids) 64 | if let contentView = infinityScrollView.subviews.first(where: { $0 is UIScrollView })?.subviews.first { 65 | contentCenterMarkerView = UIView(frame: CGRect(x: 0, y: 0, width: 1, height: infinityScrollView.frame.height)) 66 | contentCenterMarkerView.backgroundColor = UIColor.green 67 | contentCenterMarkerView.translatesAutoresizingMaskIntoConstraints = false 68 | contentView.addSubview(contentCenterMarkerView) 69 | contentCenterMarkerView.widthAnchor.constraint(equalToConstant: 1).isActive = true 70 | contentCenterMarkerView.heightAnchor.constraint(equalTo: contentView.heightAnchor).isActive = true 71 | contentCenterMarkerView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor).isActive = true 72 | contentCenterMarkerView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true 73 | } 74 | 75 | keyboardHandler = KeyboardHandler(enableCurveHack: false, heightDidChange: { [weak self] (change: KeyboardHandler.KeyboardChange) in 76 | guard let strongSelf = self else { 77 | return 78 | } 79 | UIView.animate( 80 | withDuration: change.animationDuration, 81 | delay: 0.0, 82 | animations: { 83 | UIView.setAnimationCurve(change.animationCurve) 84 | strongSelf.infinityScrollViewBottomConstraint.constant = change.newHeight 85 | strongSelf.view.layoutIfNeeded() 86 | }, 87 | completion: nil) 88 | }) 89 | infinityScrollView.reloadData() 90 | } 91 | 92 | // MARK: - Events 93 | 94 | override func viewWillAppear(_ animated: Bool) { 95 | super.viewWillAppear(animated) 96 | keyboardHandler?.isActive = true 97 | } 98 | 99 | override func viewWillDisappear(_ animated: Bool) { 100 | super.viewWillDisappear(animated) 101 | view.endEditing(true) 102 | } 103 | 104 | override func viewDidDisappear(_ animated: Bool) { 105 | super.viewDidDisappear(animated) 106 | keyboardHandler?.isActive = false 107 | } 108 | 109 | // MARK: - Interface callbacks 110 | 111 | @IBAction private func numberOfItemsSegmentedControlValueChanged() { 112 | let segmentTitle = numberOfItemsSegmentedControl.titleForSegment(at: numberOfItemsSegmentedControl.selectedSegmentIndex) 113 | if let realTitle = segmentTitle, let newNumber = Int(realTitle) { 114 | numberOfItems = newNumber 115 | numberOfItemsTextField.text = "\(numberOfItems)" 116 | infinityScrollView.reloadData() 117 | } 118 | } 119 | 120 | @IBAction private func useConstantItemWidthSwitchValueChanged() { 121 | infinityScrollView.reloadData() 122 | } 123 | 124 | @IBAction private func constantItemWidthSwitchValueChanged() { 125 | infinityScrollView.reloadData() 126 | } 127 | 128 | @IBAction private func showItemsBackgroundSwitchValueChanged() { 129 | infinityScrollView.reloadData() 130 | } 131 | 132 | @IBAction private func showVisualCenterMarkerSwitchValueChanged() { 133 | visualCenterMarkerView.isHidden = !showVisualCenterMarkerSwitch.isOn 134 | } 135 | 136 | @IBAction private func showContentCenterMarkerSwitchValueChanged() { 137 | contentCenterMarkerView.isHidden = !showContentCenterMarkerSwitch.isOn 138 | } 139 | 140 | @IBAction private func useHalfHeightSwitchValueChanged() { 141 | infinityScrollViewHeightConstraint.constant = useHalfHeightSwitch.isOn ? 100 : 200 142 | UIView.animate(withDuration: 0.3, 143 | delay: 0.0, 144 | options: [.beginFromCurrentState, .allowUserInteraction], 145 | animations: { self.infinityScrollView.superview?.layoutIfNeeded() }, 146 | completion: nil) 147 | } 148 | 149 | @IBAction private func isDecelerationEnabledSwitchValueChanged() { 150 | infinityScrollView.isDecelerationEnabled = isDecelerationEnabledSwitch.isOn 151 | } 152 | 153 | @IBAction private func useFastDecelerationRateSwitchValueChanged() { 154 | infinityScrollView.decelerationRate = useFastDecelerationRateSwitch.isOn ? .fast : .normal 155 | } 156 | 157 | @IBAction private func isSnapEnabledSwitchValueChanged() { 158 | infinityScrollView.isSnapEnabled = isSnapEnabledSwitch.isOn 159 | } 160 | 161 | @IBAction private func snapAnimationSegmentedControl1ValueChanged() { 162 | switch snapAnimationSegmentedControl1.selectedSegmentIndex { 163 | case 0: 164 | infinityScrollView.snapAnimation = .none 165 | case 1: 166 | infinityScrollView.snapAnimation = .scrollView 167 | case 2: 168 | infinityScrollView.snapAnimation = .defaultSpring 169 | case 3: 170 | infinityScrollView.snapAnimation = .curve(duration: 0.4, name: .linear) 171 | case 4: 172 | infinityScrollView.snapAnimation = .curve(duration: 0.4, name: .easeIn) 173 | default: break 174 | } 175 | snapAnimationSegmentedControl2.selectedSegmentIndex = -1 176 | } 177 | 178 | @IBAction private func snapAnimationSegmentedControl2ValueChanged() { 179 | switch snapAnimationSegmentedControl2.selectedSegmentIndex { 180 | case 0: 181 | infinityScrollView.snapAnimation = .curve(duration: 0.4, name: .easeOut) 182 | case 1: 183 | infinityScrollView.snapAnimation = .curve(duration: 0.4, name: .easeInEaseOut) 184 | case 2: 185 | infinityScrollView.snapAnimation = .curve(duration: 0.4, name: .default) 186 | case 3: 187 | infinityScrollView.snapAnimation = .spring(mass: 2, stiffness: 20, damping: 4) 188 | default: break 189 | } 190 | snapAnimationSegmentedControl1.selectedSegmentIndex = -1 191 | } 192 | 193 | @IBAction private func singleItemBehaviourSegmentedControlValueChanged() { 194 | switch singleItemBehaviourSegmentedControl.selectedSegmentIndex { 195 | case 0: 196 | infinityScrollView.setSingleItemBehavior(InfinityScrollView.SingleItemBehavior.tile) 197 | case 1: 198 | infinityScrollView.setSingleItemBehavior(InfinityScrollView.SingleItemBehavior.bounce) 199 | case 2: 200 | infinityScrollView.setSingleItemBehavior(InfinityScrollView.SingleItemBehavior.noBounce) 201 | default: break 202 | } 203 | infinityScrollView.reloadData() 204 | } 205 | 206 | } 207 | 208 | // MARK: - UITextFieldDelegate 209 | 210 | extension ExampleInfinityScrollViewController: UITextFieldDelegate { 211 | 212 | func textFieldDidEndEditing(_ textField: UITextField) { 213 | if textField === numberOfItemsTextField { 214 | var newNumber: Int = 0 215 | if let text = textField.text { 216 | newNumber = Int(text) ?? 0 217 | } 218 | numberOfItems = newNumber 219 | numberOfItemsTextField.text = "\(numberOfItems)" 220 | numberOfItemsSegmentedControl.selectedSegmentIndex = 3 221 | infinityScrollView.reloadData() 222 | } 223 | } 224 | 225 | func textFieldShouldReturn(_ textField: UITextField) -> Bool { 226 | textField.resignFirstResponder() 227 | return false 228 | } 229 | 230 | } 231 | 232 | // MARK: - InfinityScrollViewDataSource 233 | 234 | extension ExampleInfinityScrollViewController: InfinityScrollViewDataSource { 235 | 236 | func infinityScrollViewNumberOfItems(_ infinityScrollView: InfinityScrollView) -> Int { 237 | return numberOfItems 238 | } 239 | 240 | func infinityScrollView(_ infinityScrollView: InfinityScrollView, widthForItemAtIndex index: Int) -> CGFloat { 241 | if useConstantItemWidthSwitch.isOn { 242 | return constantItemWidthSwitch.isOn ? 40.0 : 200.0 243 | } else { 244 | return CGFloat.random(in: 50.0...400.0) 245 | } 246 | } 247 | 248 | func infinityScrollView(_ infinityScrollView: InfinityScrollView, viewForItemAtIndex index: Int, size: CGSize) -> UIView { 249 | let itemColors = ItemColors(itemIndex: index) 250 | let itemView = UIView(frame: CGRect(origin: CGPoint.zero, size: size)) 251 | itemView.backgroundColor = showItemsBackgroundSwitch.isOn ? itemColors.background : UIColor.clear 252 | let foregroundView = UIView(frame: itemView.bounds.inset(by: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10))) 253 | foregroundView.layer.masksToBounds = true 254 | foregroundView.layer.cornerRadius = 4.0 255 | foregroundView.backgroundColor = itemColors.foreground 256 | foregroundView.translatesAutoresizingMaskIntoConstraints = true 257 | foregroundView.autoresizingMask = [.flexibleWidth, .flexibleHeight] 258 | itemView.addSubview(foregroundView) 259 | let label = UILabel(frame: foregroundView.bounds.inset(by: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10))) 260 | label.text = "\(index)" 261 | label.textColor = itemColors.isForegroundDark ? UIColor.white : UIColor.black 262 | label.translatesAutoresizingMaskIntoConstraints = true 263 | label.autoresizingMask = [.flexibleWidth, .flexibleHeight] 264 | foregroundView.addSubview(label) 265 | return itemView 266 | } 267 | 268 | } 269 | 270 | // MARK: - InfinityScrollViewDelegate 271 | 272 | extension ExampleInfinityScrollViewController: InfinityScrollViewDelegate { 273 | 274 | func infinityScrollView(_ infinityScrollView: InfinityScrollView, 275 | willEndSwipeOnItemAtIndex itemIndex: Int, 276 | swipeDirection: InfinityScrollView.SwipeDirection) { 277 | print("delegate: willEndSwipeOnItemAtIndex: \(itemIndex) swipeDirection: \(swipeDirection)") 278 | } 279 | 280 | func infinityScrollView(_ infinityScrollView: InfinityScrollView, didEndDeceleratingOnItemAtIndex itemIndex: Int, wasAborted: Bool) { 281 | print("delegate: didEndDeceleratingOnItemAtIndex: \(itemIndex), wasAborted: \(wasAborted)") 282 | } 283 | 284 | func infinityScrollView(_ infinityScrollView: InfinityScrollView, didSelectItemAtIndex itemIndex: Int) { 285 | print("delegate: didSelectItemAtIndex: \(itemIndex)") 286 | } 287 | 288 | } 289 | 290 | // MARK: - ItemColors 291 | 292 | extension ExampleInfinityScrollViewController { 293 | 294 | private struct ItemColors { 295 | 296 | internal let background: UIColor 297 | internal let foreground: UIColor 298 | internal let isForegroundDark: Bool 299 | 300 | internal init(itemIndex: Int) { 301 | let step: UInt32 = 0x11 * 40 // 0x11 302 | let maxIndex: UInt32 = 0xFFFFFF / step / 2 - 1 303 | let index = UInt32(itemIndex) % maxIndex 304 | background = UIColor(rgbDecimalColor: (index * 2 + 0) * step) 305 | foreground = UIColor(rgbDecimalColor: (index * 2 + 1) * step) 306 | var red: CGFloat = 0 307 | var green: CGFloat = 0 308 | var blue: CGFloat = 0 309 | var alpha: CGFloat = 0 310 | foreground.getRed(&red, green: &green, blue: &blue, alpha: &alpha) 311 | let brightness = (red / 255.0) * 0.3 + (green / 255.0) * 0.59 + (blue / 255.0) * 0.11 312 | isForegroundDark = brightness < 0.3 313 | } 314 | 315 | } 316 | 317 | } 318 | -------------------------------------------------------------------------------- /InfinityScrollView_Framework.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | A582C61C2799266D005AC4E8 /* InfinityScrollView_Framework.h in Headers */ = {isa = PBXBuildFile; fileRef = A582C61B2799266D005AC4E8 /* InfinityScrollView_Framework.h */; settings = {ATTRIBUTES = (Public, ); }; }; 11 | A582C626279928E1005AC4E8 /* InfinityScrollView+Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = A582C623279928E1005AC4E8 /* InfinityScrollView+Models.swift */; }; 12 | A582C627279928E1005AC4E8 /* SingleTouchDownGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A582C624279928E1005AC4E8 /* SingleTouchDownGestureRecognizer.swift */; }; 13 | A582C628279928E1005AC4E8 /* InfinityScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A582C625279928E1005AC4E8 /* InfinityScrollView.swift */; }; 14 | /* End PBXBuildFile section */ 15 | 16 | /* Begin PBXFileReference section */ 17 | A582C6182799266D005AC4E8 /* InfinityScrollView_Framework.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = InfinityScrollView_Framework.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 18 | A582C61B2799266D005AC4E8 /* InfinityScrollView_Framework.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = InfinityScrollView_Framework.h; sourceTree = ""; }; 19 | A582C623279928E1005AC4E8 /* InfinityScrollView+Models.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "InfinityScrollView+Models.swift"; sourceTree = ""; }; 20 | A582C624279928E1005AC4E8 /* SingleTouchDownGestureRecognizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SingleTouchDownGestureRecognizer.swift; sourceTree = ""; }; 21 | A582C625279928E1005AC4E8 /* InfinityScrollView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InfinityScrollView.swift; sourceTree = ""; }; 22 | /* End PBXFileReference section */ 23 | 24 | /* Begin PBXFrameworksBuildPhase section */ 25 | A582C6152799266D005AC4E8 /* Frameworks */ = { 26 | isa = PBXFrameworksBuildPhase; 27 | buildActionMask = 2147483647; 28 | files = ( 29 | ); 30 | runOnlyForDeploymentPostprocessing = 0; 31 | }; 32 | /* End PBXFrameworksBuildPhase section */ 33 | 34 | /* Begin PBXGroup section */ 35 | 3691320BB850FC7DA80CFCBD /* Pods */ = { 36 | isa = PBXGroup; 37 | children = ( 38 | ); 39 | path = Pods; 40 | sourceTree = ""; 41 | }; 42 | A582C60E2799266C005AC4E8 = { 43 | isa = PBXGroup; 44 | children = ( 45 | A582C622279928E1005AC4E8 /* Source */, 46 | A582C61A2799266D005AC4E8 /* InfinityScrollView_Framework */, 47 | A582C6192799266D005AC4E8 /* Products */, 48 | 3691320BB850FC7DA80CFCBD /* Pods */, 49 | ); 50 | sourceTree = ""; 51 | }; 52 | A582C6192799266D005AC4E8 /* Products */ = { 53 | isa = PBXGroup; 54 | children = ( 55 | A582C6182799266D005AC4E8 /* InfinityScrollView_Framework.framework */, 56 | ); 57 | name = Products; 58 | sourceTree = ""; 59 | }; 60 | A582C61A2799266D005AC4E8 /* InfinityScrollView_Framework */ = { 61 | isa = PBXGroup; 62 | children = ( 63 | A582C61B2799266D005AC4E8 /* InfinityScrollView_Framework.h */, 64 | ); 65 | path = InfinityScrollView_Framework; 66 | sourceTree = ""; 67 | }; 68 | A582C622279928E1005AC4E8 /* Source */ = { 69 | isa = PBXGroup; 70 | children = ( 71 | A582C625279928E1005AC4E8 /* InfinityScrollView.swift */, 72 | A582C623279928E1005AC4E8 /* InfinityScrollView+Models.swift */, 73 | A582C624279928E1005AC4E8 /* SingleTouchDownGestureRecognizer.swift */, 74 | ); 75 | path = Source; 76 | sourceTree = SOURCE_ROOT; 77 | }; 78 | /* End PBXGroup section */ 79 | 80 | /* Begin PBXHeadersBuildPhase section */ 81 | A582C6132799266D005AC4E8 /* Headers */ = { 82 | isa = PBXHeadersBuildPhase; 83 | buildActionMask = 2147483647; 84 | files = ( 85 | A582C61C2799266D005AC4E8 /* InfinityScrollView_Framework.h in Headers */, 86 | ); 87 | runOnlyForDeploymentPostprocessing = 0; 88 | }; 89 | /* End PBXHeadersBuildPhase section */ 90 | 91 | /* Begin PBXNativeTarget section */ 92 | A582C6172799266D005AC4E8 /* InfinityScrollView_Framework */ = { 93 | isa = PBXNativeTarget; 94 | buildConfigurationList = A582C61F2799266D005AC4E8 /* Build configuration list for PBXNativeTarget "InfinityScrollView_Framework" */; 95 | buildPhases = ( 96 | A582C6132799266D005AC4E8 /* Headers */, 97 | A582C6142799266D005AC4E8 /* Sources */, 98 | A582C6152799266D005AC4E8 /* Frameworks */, 99 | A582C6162799266D005AC4E8 /* Resources */, 100 | ); 101 | buildRules = ( 102 | ); 103 | dependencies = ( 104 | ); 105 | name = InfinityScrollView_Framework; 106 | productName = InfinityScrollView_Framework; 107 | productReference = A582C6182799266D005AC4E8 /* InfinityScrollView_Framework.framework */; 108 | productType = "com.apple.product-type.framework"; 109 | }; 110 | /* End PBXNativeTarget section */ 111 | 112 | /* Begin PBXProject section */ 113 | A582C60F2799266C005AC4E8 /* Project object */ = { 114 | isa = PBXProject; 115 | attributes = { 116 | BuildIndependentTargetsInParallel = 1; 117 | LastUpgradeCheck = 1320; 118 | TargetAttributes = { 119 | A582C6172799266D005AC4E8 = { 120 | CreatedOnToolsVersion = 13.2.1; 121 | }; 122 | }; 123 | }; 124 | buildConfigurationList = A582C6122799266D005AC4E8 /* Build configuration list for PBXProject "InfinityScrollView_Framework" */; 125 | compatibilityVersion = "Xcode 13.0"; 126 | developmentRegion = en; 127 | hasScannedForEncodings = 0; 128 | knownRegions = ( 129 | en, 130 | Base, 131 | ); 132 | mainGroup = A582C60E2799266C005AC4E8; 133 | productRefGroup = A582C6192799266D005AC4E8 /* Products */; 134 | projectDirPath = ""; 135 | projectRoot = ""; 136 | targets = ( 137 | A582C6172799266D005AC4E8 /* InfinityScrollView_Framework */, 138 | ); 139 | }; 140 | /* End PBXProject section */ 141 | 142 | /* Begin PBXResourcesBuildPhase section */ 143 | A582C6162799266D005AC4E8 /* Resources */ = { 144 | isa = PBXResourcesBuildPhase; 145 | buildActionMask = 2147483647; 146 | files = ( 147 | ); 148 | runOnlyForDeploymentPostprocessing = 0; 149 | }; 150 | /* End PBXResourcesBuildPhase section */ 151 | 152 | /* Begin PBXSourcesBuildPhase section */ 153 | A582C6142799266D005AC4E8 /* Sources */ = { 154 | isa = PBXSourcesBuildPhase; 155 | buildActionMask = 2147483647; 156 | files = ( 157 | A582C626279928E1005AC4E8 /* InfinityScrollView+Models.swift in Sources */, 158 | A582C627279928E1005AC4E8 /* SingleTouchDownGestureRecognizer.swift in Sources */, 159 | A582C628279928E1005AC4E8 /* InfinityScrollView.swift in Sources */, 160 | ); 161 | runOnlyForDeploymentPostprocessing = 0; 162 | }; 163 | /* End PBXSourcesBuildPhase section */ 164 | 165 | /* Begin XCBuildConfiguration section */ 166 | A582C61D2799266D005AC4E8 /* Debug */ = { 167 | isa = XCBuildConfiguration; 168 | buildSettings = { 169 | ALWAYS_SEARCH_USER_PATHS = NO; 170 | CLANG_ANALYZER_NONNULL = YES; 171 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 172 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 173 | CLANG_CXX_LIBRARY = "libc++"; 174 | CLANG_ENABLE_MODULES = YES; 175 | CLANG_ENABLE_OBJC_ARC = YES; 176 | CLANG_ENABLE_OBJC_WEAK = YES; 177 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 178 | CLANG_WARN_BOOL_CONVERSION = YES; 179 | CLANG_WARN_COMMA = YES; 180 | CLANG_WARN_CONSTANT_CONVERSION = YES; 181 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 182 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 183 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 184 | CLANG_WARN_EMPTY_BODY = YES; 185 | CLANG_WARN_ENUM_CONVERSION = YES; 186 | CLANG_WARN_INFINITE_RECURSION = YES; 187 | CLANG_WARN_INT_CONVERSION = YES; 188 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 189 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 190 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 191 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 192 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 193 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 194 | CLANG_WARN_STRICT_PROTOTYPES = YES; 195 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 196 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 197 | CLANG_WARN_UNREACHABLE_CODE = YES; 198 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 199 | COPY_PHASE_STRIP = NO; 200 | CURRENT_PROJECT_VERSION = 1; 201 | DEBUG_INFORMATION_FORMAT = dwarf; 202 | ENABLE_STRICT_OBJC_MSGSEND = YES; 203 | ENABLE_TESTABILITY = YES; 204 | GCC_C_LANGUAGE_STANDARD = gnu11; 205 | GCC_DYNAMIC_NO_PIC = NO; 206 | GCC_NO_COMMON_BLOCKS = YES; 207 | GCC_OPTIMIZATION_LEVEL = 0; 208 | GCC_PREPROCESSOR_DEFINITIONS = ( 209 | "DEBUG=1", 210 | "$(inherited)", 211 | ); 212 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 213 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 214 | GCC_WARN_UNDECLARED_SELECTOR = YES; 215 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 216 | GCC_WARN_UNUSED_FUNCTION = YES; 217 | GCC_WARN_UNUSED_VARIABLE = YES; 218 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 219 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 220 | MTL_FAST_MATH = YES; 221 | ONLY_ACTIVE_ARCH = YES; 222 | SDKROOT = iphoneos; 223 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 224 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 225 | VERSIONING_SYSTEM = "apple-generic"; 226 | VERSION_INFO_PREFIX = ""; 227 | }; 228 | name = Debug; 229 | }; 230 | A582C61E2799266D005AC4E8 /* Release */ = { 231 | isa = XCBuildConfiguration; 232 | buildSettings = { 233 | ALWAYS_SEARCH_USER_PATHS = NO; 234 | CLANG_ANALYZER_NONNULL = YES; 235 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 236 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 237 | CLANG_CXX_LIBRARY = "libc++"; 238 | CLANG_ENABLE_MODULES = YES; 239 | CLANG_ENABLE_OBJC_ARC = YES; 240 | CLANG_ENABLE_OBJC_WEAK = YES; 241 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 242 | CLANG_WARN_BOOL_CONVERSION = YES; 243 | CLANG_WARN_COMMA = YES; 244 | CLANG_WARN_CONSTANT_CONVERSION = YES; 245 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 246 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 247 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 248 | CLANG_WARN_EMPTY_BODY = YES; 249 | CLANG_WARN_ENUM_CONVERSION = YES; 250 | CLANG_WARN_INFINITE_RECURSION = YES; 251 | CLANG_WARN_INT_CONVERSION = YES; 252 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 253 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 254 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 255 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 256 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 257 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 258 | CLANG_WARN_STRICT_PROTOTYPES = YES; 259 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 260 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 261 | CLANG_WARN_UNREACHABLE_CODE = YES; 262 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 263 | COPY_PHASE_STRIP = NO; 264 | CURRENT_PROJECT_VERSION = 1; 265 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 266 | ENABLE_NS_ASSERTIONS = NO; 267 | ENABLE_STRICT_OBJC_MSGSEND = YES; 268 | GCC_C_LANGUAGE_STANDARD = gnu11; 269 | GCC_NO_COMMON_BLOCKS = YES; 270 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 271 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 272 | GCC_WARN_UNDECLARED_SELECTOR = YES; 273 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 274 | GCC_WARN_UNUSED_FUNCTION = YES; 275 | GCC_WARN_UNUSED_VARIABLE = YES; 276 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 277 | MTL_ENABLE_DEBUG_INFO = NO; 278 | MTL_FAST_MATH = YES; 279 | SDKROOT = iphoneos; 280 | SWIFT_COMPILATION_MODE = wholemodule; 281 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 282 | VALIDATE_PRODUCT = YES; 283 | VERSIONING_SYSTEM = "apple-generic"; 284 | VERSION_INFO_PREFIX = ""; 285 | }; 286 | name = Release; 287 | }; 288 | A582C6202799266D005AC4E8 /* Debug */ = { 289 | isa = XCBuildConfiguration; 290 | buildSettings = { 291 | CODE_SIGN_STYLE = Automatic; 292 | CURRENT_PROJECT_VERSION = 1; 293 | DEFINES_MODULE = YES; 294 | DYLIB_COMPATIBILITY_VERSION = 1; 295 | DYLIB_CURRENT_VERSION = 1; 296 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 297 | GENERATE_INFOPLIST_FILE = YES; 298 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 299 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 300 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 301 | LD_RUNPATH_SEARCH_PATHS = ( 302 | "$(inherited)", 303 | "@executable_path/Frameworks", 304 | "@loader_path/Frameworks", 305 | ); 306 | MARKETING_VERSION = 1.0; 307 | PRODUCT_BUNDLE_IDENTIFIER = "com.shakuro.InfinityScrollView-Framework"; 308 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 309 | SKIP_INSTALL = YES; 310 | SWIFT_EMIT_LOC_STRINGS = YES; 311 | SWIFT_VERSION = 5.0; 312 | TARGETED_DEVICE_FAMILY = "1,2"; 313 | }; 314 | name = Debug; 315 | }; 316 | A582C6212799266D005AC4E8 /* Release */ = { 317 | isa = XCBuildConfiguration; 318 | buildSettings = { 319 | CODE_SIGN_STYLE = Automatic; 320 | CURRENT_PROJECT_VERSION = 1; 321 | DEFINES_MODULE = YES; 322 | DYLIB_COMPATIBILITY_VERSION = 1; 323 | DYLIB_CURRENT_VERSION = 1; 324 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 325 | GENERATE_INFOPLIST_FILE = YES; 326 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 327 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 328 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 329 | LD_RUNPATH_SEARCH_PATHS = ( 330 | "$(inherited)", 331 | "@executable_path/Frameworks", 332 | "@loader_path/Frameworks", 333 | ); 334 | MARKETING_VERSION = 1.0; 335 | PRODUCT_BUNDLE_IDENTIFIER = "com.shakuro.InfinityScrollView-Framework"; 336 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 337 | SKIP_INSTALL = YES; 338 | SWIFT_EMIT_LOC_STRINGS = YES; 339 | SWIFT_VERSION = 5.0; 340 | TARGETED_DEVICE_FAMILY = "1,2"; 341 | }; 342 | name = Release; 343 | }; 344 | /* End XCBuildConfiguration section */ 345 | 346 | /* Begin XCConfigurationList section */ 347 | A582C6122799266D005AC4E8 /* Build configuration list for PBXProject "InfinityScrollView_Framework" */ = { 348 | isa = XCConfigurationList; 349 | buildConfigurations = ( 350 | A582C61D2799266D005AC4E8 /* Debug */, 351 | A582C61E2799266D005AC4E8 /* Release */, 352 | ); 353 | defaultConfigurationIsVisible = 0; 354 | defaultConfigurationName = Release; 355 | }; 356 | A582C61F2799266D005AC4E8 /* Build configuration list for PBXNativeTarget "InfinityScrollView_Framework" */ = { 357 | isa = XCConfigurationList; 358 | buildConfigurations = ( 359 | A582C6202799266D005AC4E8 /* Debug */, 360 | A582C6212799266D005AC4E8 /* Release */, 361 | ); 362 | defaultConfigurationIsVisible = 0; 363 | defaultConfigurationName = Release; 364 | }; 365 | /* End XCConfigurationList section */ 366 | }; 367 | rootObject = A582C60F2799266C005AC4E8 /* Project object */; 368 | } 369 | -------------------------------------------------------------------------------- /InfinityScrollView_Framework.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /InfinityScrollView_Framework.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /InfinityScrollView_Framework/InfinityScrollView_Framework.h: -------------------------------------------------------------------------------- 1 | // 2 | // InfinityScrollView_Framework.h 3 | // InfinityScrollView_Framework 4 | // 5 | // Created by Sergey Popov on 1/20/22. 6 | // 7 | 8 | #import 9 | 10 | //! Project version number for InfinityScrollView_Framework. 11 | FOUNDATION_EXPORT double InfinityScrollView_FrameworkVersionNumber; 12 | 13 | //! Project version string for InfinityScrollView_Framework. 14 | FOUNDATION_EXPORT const unsigned char InfinityScrollView_FrameworkVersionString[]; 15 | 16 | // In this header, you should import all the public headers of your framework using statements like #import 17 | 18 | 19 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2021 Shakuro (https://shakuro.com/) 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 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | source 'https://github.com/CocoaPods/Specs.git' 2 | 3 | platform :ios, '11.0' 4 | 5 | use_frameworks! 6 | 7 | workspace 'InfinityScrollView' 8 | 9 | target 'InfinityScrollView_Framework' do 10 | project 'InfinityScrollView_Framework.xcodeproj' 11 | pod 'Shakuro.CommonTypes', '1.1.4' 12 | end 13 | 14 | target 'InfinityScrollView_Example' do 15 | project 'InfinityScrollView_Example.xcodeproj' 16 | pod 'SwiftLint', '0.43.1' 17 | pod 'Shakuro.CommonTypes', '1.1.4' 18 | end 19 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Shakuro.CommonTypes (1.1.4) 3 | - SwiftLint (0.43.1) 4 | 5 | DEPENDENCIES: 6 | - Shakuro.CommonTypes (= 1.1.4) 7 | - SwiftLint (= 0.43.1) 8 | 9 | SPEC REPOS: 10 | https://github.com/CocoaPods/Specs.git: 11 | - Shakuro.CommonTypes 12 | - SwiftLint 13 | 14 | SPEC CHECKSUMS: 15 | Shakuro.CommonTypes: a3c3d432a2fc19e3e7971dad13aa9066d7ce5771 16 | SwiftLint: 99f82d07b837b942dd563c668de129a03fc3fb52 17 | 18 | PODFILE CHECKSUM: 7f901854237e71d11baa22b52b3e4705cfe4c847 19 | 20 | COCOAPODS: 1.11.3 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Shakuro Infinity Scroll View](Resources/title_image.png) 2 |

3 | # InfinityScrollView 4 | ![Version](https://img.shields.io/badge/version-1.0.0-blue.svg) 5 | ![Platform](https://img.shields.io/badge/platform-iOS-lightgrey.svg) 6 | ![License MIT](https://img.shields.io/badge/license-MIT-green.svg) 7 | 8 | - [Requirements](#requirements) 9 | - [Installation](#installation) 10 | - [Usage](#usage) 11 | - [License](#license) 12 | 13 | `InfinityScrollView` is a Swift library that allows you to add endless horizontal scroll to different items. `InfinityScrollView` has various configuration options: 14 | - fast deceleration rate 15 | - snap to center item 16 | - configurable snap deceleration animations 17 | 18 | 19 | Infinity scroll example: 20 | 21 | ![](Resources/infinity_scroll.gif) 22 | 23 | 24 | Infinity scroll example with different items sizes: 25 | 26 | ![](Resources/infinity_scroll_with_different_sizes.gif) 27 | 28 | 29 | Infinity scroll example with snap to center item: 30 | 31 | ![](Resources/infinity_scroll_with_snap_to_center.gif) 32 | 33 | 34 | Single item behaviour: 35 | 36 | ![](Resources/single_item_behaviour.gif) 37 | 38 | 39 | ## Requirements 40 | 41 | - iOS 11.0+ 42 | - Xcode 11.0+ 43 | - Swift 5.0+ 44 | 45 | ## Installation 46 | 47 | ### CocoaPods 48 | 49 | To integrate Infinity Scroll View into your Xcode project with CocoaPods, specify it in your `Podfile`: 50 | 51 | ```ruby 52 | pod 'Shakuro.InfinityScrollView' 53 | ``` 54 | 55 | Then, run the following command: 56 | 57 | ```bash 58 | $ pod install 59 | ``` 60 | 61 | ### Manually 62 | 63 | If you prefer not to use CocoaPods, you can integrate Shakuro.InfinityScrollView simply by copying it to your project. 64 | 65 | ## Usage 66 | 67 | Just create `InfinityScrollView` programmatically or in the storyboard. Take into account that `InfinityScrollView` must have the data source and the delegate objects. The data source needs to adopt the `InfinityScrollViewDataSource` protocol and the delegate has to adopt the `InfinityScrollViewDelegate` protocol. The data source provides the views that `InfinityScrollView` will display. The delegate allows you to respond to scrolling events. 68 | 69 | Take a look at the [InfinityScrollView_Example](https://github.com/shakurocom/InfinityScrollView/tree/main/InfinityScrollView_Example) (you need to perform `pod install` before before using it). 70 | 71 | ## License 72 | 73 | Shakuro.InfinityScrollView is released under the MIT license. [See LICENSE](https://github.com/shakurocom/InfinityScrollView/blob/main/LICENSE.md) for details. 74 | 75 | ## Give it a try and reach us 76 | 77 | Explore our expertise in Native Mobile Development and iOS Development.

78 | 79 | If you need professional assistance with your mobile or web project, feel free to contact our team 80 | 81 | 82 | -------------------------------------------------------------------------------- /Resources/infinity_scroll.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shakurocom/InfinityScrollView/4820d1554107ad72b054c35b1d70b594d3026173/Resources/infinity_scroll.gif -------------------------------------------------------------------------------- /Resources/infinity_scroll_with_different_sizes.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shakurocom/InfinityScrollView/4820d1554107ad72b054c35b1d70b594d3026173/Resources/infinity_scroll_with_different_sizes.gif -------------------------------------------------------------------------------- /Resources/infinity_scroll_with_snap_to_center.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shakurocom/InfinityScrollView/4820d1554107ad72b054c35b1d70b594d3026173/Resources/infinity_scroll_with_snap_to_center.gif -------------------------------------------------------------------------------- /Resources/single_item_behaviour.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shakurocom/InfinityScrollView/4820d1554107ad72b054c35b1d70b594d3026173/Resources/single_item_behaviour.gif -------------------------------------------------------------------------------- /Resources/title_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shakurocom/InfinityScrollView/4820d1554107ad72b054c35b1d70b594d3026173/Resources/title_image.png -------------------------------------------------------------------------------- /Shakuro.InfinityScrollView.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'Shakuro.InfinityScrollView' 3 | s.version = '1.0.4' 4 | s.summary = 'Shakuro Infinity Scroll View' 5 | s.homepage = 'https://github.com/shakurocom/InfinityScrollView' 6 | s.license = { :type => "MIT", :file => "LICENSE.md" } 7 | s.authors = {'apopov1988' => 'apopov@shakuro.com', 'wwwpix' => 'spopov@shakuro.com', 'slaschuk' => 'slaschuk@shakuro.com'} 8 | s.source = { :git => 'https://github.com/shakurocom/InfinityScrollView.git', :tag => s.version } 9 | s.source_files = 'Source/*', 'Source/**/*' 10 | s.swift_version = ['5.1', '5.2', '5.3', '5.4', '5.5', '5.6'] 11 | s.ios.deployment_target = '11.0' 12 | 13 | s.dependency 'Shakuro.CommonTypes', '~> 1.1' 14 | 15 | end 16 | -------------------------------------------------------------------------------- /Source/InfinityScrollView+Models.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Shakuro (https://shakuro.com/) 3 | // Sergey Laschuk 4 | // 5 | 6 | import CoreGraphics 7 | import Foundation 8 | import UIKit 9 | 10 | extension InfinityScrollView { 11 | 12 | public enum SwipeDirection { 13 | 14 | /// User dragged here and there but end drag on the same item he started. 15 | case none 16 | 17 | /// User ended dragging on item to the left of the starting item 18 | case left 19 | 20 | /// User ended dragging on item to the right of the starting item 21 | case right 22 | 23 | } 24 | 25 | public enum SnapAnimation { 26 | 27 | /// No animation - hard jump to projected offset 28 | case none 29 | 30 | /// Default animation of UIScrollView 31 | case scrollView 32 | 33 | /// Animation curve with given name (ex.: easeIn) 34 | case curve(duration: CFTimeInterval, name: CAMediaTimingFunctionName) 35 | 36 | /// Dumped spring. See `CASpringAnimation` for parameters description. 37 | /// Initial velocity obtained from drag. 38 | /// - warning: Very bouncy spring on energetic (high velocity) drag can lead to user see not yet tiled-out area. 39 | case spring(mass: CGFloat, stiffness: CGFloat, damping: CGFloat) 40 | 41 | /// example parameters for spring animation 42 | public static let defaultSpring = SnapAnimation.spring(mass: 1, stiffness: 40, damping: 8) 43 | 44 | } 45 | 46 | public enum SingleItemBehavior { 47 | 48 | /// Single item is tiled (as if there is more than one item) 49 | case tile 50 | 51 | /// No tiling. 52 | /// Scroll area set to the width of item. 53 | /// UIScrollview's bounce will be enabled for user drag. 54 | case bounce 55 | 56 | /// No tiling. 57 | /// Scroll area set to the width of item. 58 | /// UIScrollview's bounce will be disabled for user drag. 59 | case noBounce 60 | 61 | } 62 | 63 | internal struct NearestVisibleCenterItemData { 64 | internal let anchorOffsetX: CGFloat 65 | internal let tileCenterX: CGFloat 66 | internal let itemIndex: Int 67 | internal let tileIndex: Int 68 | } 69 | 70 | internal struct AnimationData { 71 | internal let projectedContentOffsetX: CGFloat 72 | internal let nearestItemData: NearestVisibleCenterItemData 73 | internal let initialVelocityX: CGFloat 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /Source/InfinityScrollView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2020 Shakuro (https://shakuro.com/) 3 | // Sergey Popov, Sergey Laschuk 4 | // 5 | 6 | import Foundation 7 | import Shakuro_CommonTypes 8 | import UIKit 9 | 10 | public protocol InfinityScrollViewDataSource: AnyObject { 11 | 12 | /// - parameter infinityScrollView: caller. 13 | /// - returns: number of elementes in infinity scroll. 14 | /// Negative value will throw assert (debug) or be rounded to zero (release). 15 | func infinityScrollViewNumberOfItems(_ infinityScrollView: InfinityScrollView) -> Int 16 | 17 | /// - parameter infinityScrollView: caller. 18 | /// - parameter index: index of item. 19 | /// - returns: width of item at index. 20 | /// Behaviour for zero/negative values is not defined (please refrain) 21 | /// Height will be equal to the height of InfinityScrollView. 22 | func infinityScrollView(_ infinityScrollView: InfinityScrollView, widthForItemAtIndex index: Int) -> CGFloat 23 | 24 | /// - parameter infinityScrollView: caller. 25 | /// - parameter index: index of item. 26 | /// - parameter size: size, that will be set to view. 27 | /// - returns: setted up view for displaying item 28 | /// 29 | /// `.intrinsicContentSize` is not supported. 30 | /// Height will be always equal to height of InfinityScrollView itself. 31 | /// Consider creating etalon view and calculating dynamic width inside `infinityScrollView(:,widthForItemAtIndex:)`. 32 | /// 33 | /// You can perform some additional configuration of returned view, depending on the provided size. 34 | func infinityScrollView(_ infinityScrollView: InfinityScrollView, viewForItemAtIndex index: Int, size: CGSize) -> UIView 35 | 36 | } 37 | 38 | public protocol InfinityScrollViewDelegate: AnyObject { 39 | 40 | /// Will not be reported if there are no items in InfinityScrollView 41 | func infinityScrollView(_ infinityScrollView: InfinityScrollView, 42 | willEndSwipeOnItemAtIndex itemIndex: Int, 43 | swipeDirection: InfinityScrollView.SwipeDirection) 44 | 45 | /// Will be reported even if deceleration is disabled. 46 | func infinityScrollView(_ infinityScrollView: InfinityScrollView, didEndDeceleratingOnItemAtIndex itemIndex: Int, wasAborted: Bool) 47 | 48 | /// User tapped specific item. 49 | /// Tap to stop deceleration animation do not count. 50 | func infinityScrollView(_ infinityScrollView: InfinityScrollView, didSelectItemAtIndex itemIndex: Int) 51 | 52 | } 53 | 54 | /// CollectView-like control. Handles infinity collection of items. 55 | /// Can be scrolled left and right. 56 | /// Looped: after last items goes first one. 57 | /// If only single item - user will see endless amounts of this item. 58 | public class InfinityScrollView: UIView { 59 | 60 | private enum Constant { 61 | static let snapAnimationKey: String = "InfinityScrollView.snapAnimation" 62 | static let snapAnimationNameValue: String = "InfinityScrollView.snapAnimation.name" 63 | static let snapAnimationNameKey: String = "InfinityScrollView.snapAnimation.key" 64 | } 65 | 66 | public weak var dataSource: InfinityScrollViewDataSource? 67 | public weak var delegate: InfinityScrollViewDelegate? 68 | 69 | /// If `true` content will be decelerated with velocity of drag (after drag is ended) 70 | /// 71 | /// Default value is `true`. 72 | public var isDecelerationEnabled: Bool = true 73 | 74 | /// Deceleration rate. 75 | /// Use constants from `UIScrollView.DecelerationRate` as guide for possible values. 76 | /// 77 | /// Default value is `UIScrollView.DecelerationRate.normal`. 78 | public var decelerationRate: UIScrollView.DecelerationRate { 79 | get { 80 | return internalScrollView.decelerationRate 81 | } 82 | set { 83 | internalScrollView.decelerationRate = newValue 84 | } 85 | } 86 | 87 | /// If `true` content will be snapped to nearest item center at drag end. Respects deceleration. 88 | /// Applies animation if allowed. 89 | /// Applies deceleration if allowed. 90 | /// Not recommended if item's width is greater than control's width. 91 | /// 92 | /// Default value is `false`. 93 | public var isSnapEnabled: Bool = false 94 | 95 | /// Animation for snap after dragging ended 96 | /// 97 | /// Default value is `SnapAnimation.scrollView` 98 | public var snapAnimation: SnapAnimation = .scrollView 99 | 100 | /// Behaviour for data source of only one item. 101 | /// 102 | /// Default value is `SingleItemBehavior.bounce`. 103 | public private(set) var singleItemBehavior: SingleItemBehavior = .bounce 104 | 105 | private var internalScrollView: UIScrollView! 106 | private var contentContainerView: UIView! 107 | private var touchDownRecognizer: SingleTouchDownGestureRecognizer! 108 | private var tapRecognizer: UITapGestureRecognizer! 109 | 110 | private var cachedItemWidths: [CGFloat] = [] 111 | private var cachedNumberOfItems: Int = 0 112 | private var cachedItemsTotalWidth: CGFloat = 0.0 113 | 114 | /// Offset origin.x of view of first item of first iteration from content's centerX. 115 | /// 116 | /// -(cachedItems[0].width)/2 117 | private var cachedZeroItemOffset: CGFloat = 0.0 118 | 119 | /// How much extra space is filled with item's views beyond visible area (internalScrollView.bounds) to the left & right 120 | /// 121 | /// = min(bounds.width, 500) 122 | private var visibleAreaOverhangX: CGFloat = 500.0 // twice frame's width 123 | 124 | /// Views for items that are added to content view. 125 | /// Index is a tiled (zero-based & pass-through) index: 11 for 9 total items means 3rd item in second iteration. 126 | private var visibleTileViews: [Int: UIView] = [:] 127 | 128 | /// Minimum of content offset change required to trigger recenter of scrollable content. 129 | /// 130 | /// visibleAreaOverhangX / 2 131 | private var recenterThrottleDistance: CGFloat = 0.0 132 | 133 | private var allowRecenter: Int = 1 // allowed if > 0 134 | 135 | /// The whole width of scrollable area. Should be big enough for user not to "bounce" against it. 136 | /// Offset will be resetted within it as often as possible, so that user constantly see only ceter-ish section of it. 137 | /// 138 | /// 5000.0 139 | private var scrollableContentWidth: CGFloat = 5000.0 140 | 141 | /// Layout will be skipped, if lastLayoutBoundsSize == bounds.size 142 | private var lastLayoutBoundsSize: CGSize = .zero 143 | 144 | /// Minimum amount content offset must be changed to trigger views tiling. 145 | /// 146 | /// visibleAreaOverhangX / 5 147 | private var tileThrottleDistance: CGFloat = 0.0 148 | 149 | /// Last time tiling was performed - content offset was here. 150 | /// Used for throttling of tiling. 151 | private var tileDoneForContentOffsetX: CGFloat = .infinity 152 | 153 | /// allowed if > 0 154 | private var allowTiling: Int = 1 155 | 156 | /// Additional offset accumulated due to recentering. 157 | private var recenteredZeroItemOffset: CGFloat = 0 158 | 159 | /// Will be filled/updated in `scrollViewWillEndDragging(:withVelocity:targetContentOffset:)`. 160 | /// Used for reporting to delegate and animating deceleration. 161 | private var decelerationAnimationData: AnimationData? 162 | 163 | /// Animation used for animating deceleration (snap or no snap). 164 | /// `.none` & `.scrollView` settings do not use this property. 165 | private var decelerateAnimation: CAAnimation? 166 | 167 | /// Index of the tile under the center 168 | private var dragStartedTileIndex: Int? 169 | 170 | // MARK: - Initialization 171 | 172 | override init(frame: CGRect) { 173 | super.init(frame: frame) 174 | commonInit() 175 | } 176 | 177 | required init?(coder aDecoder: NSCoder) { 178 | super.init(coder: aDecoder) 179 | commonInit() 180 | } 181 | 182 | // MARK: - Events 183 | 184 | public override func layoutSubviews() { 185 | super.layoutSubviews() 186 | 187 | guard lastLayoutBoundsSize != bounds.size else { 188 | return 189 | } 190 | 191 | updateScroll() 192 | 193 | // width-dependent layout 194 | if lastLayoutBoundsSize.width != bounds.width { 195 | visibleAreaOverhangX = max(500.0, bounds.width) 196 | tileThrottleDistance = visibleAreaOverhangX / 5.0 197 | recenterThrottleDistance = visibleAreaOverhangX / 2.0 198 | recenterIfNeeded(allowThrottle: false, allowShiftTileViews: false) 199 | tileItemViewsIfNeeded(allowThrottle: false, targetContentOffsetX: nil) 200 | setupSingleItemIfNeeded() 201 | } 202 | 203 | // height-dependent layout 204 | if lastLayoutBoundsSize.height != bounds.height { 205 | updateVisibleTileViews(height: bounds.height) 206 | } 207 | 208 | lastLayoutBoundsSize = bounds.size 209 | } 210 | 211 | // MARK: - Public 212 | 213 | public func viewForItem(at index: Int) -> UIView? { 214 | return visibleTileViews[index] 215 | } 216 | 217 | public func accessibilityScrollForward() { 218 | guard visibleTileViews.count > 1, 219 | let currentIndex = indexOfItemAtVisibleCenter() 220 | else { 221 | return 222 | } 223 | let nextIndex = currentIndex + 1 == visibleTileViews.count ? 0 : currentIndex + 1 224 | if let frame = visibleTileViews[nextIndex]?.frame { 225 | internalScrollView.setContentOffset(CGPoint(x: frame.midX - (internalScrollView.frame.width / 2), y: 0), animated: true) 226 | } 227 | } 228 | 229 | public func accessibilityScrollBackward() { 230 | guard visibleTileViews.count > 1, 231 | let currentIndex = indexOfItemAtVisibleCenter() 232 | else { 233 | return 234 | } 235 | let previousIndex = currentIndex - 1 == -1 ? visibleTileViews.count - 1 : currentIndex - 1 236 | if let frame = visibleTileViews[previousIndex]?.frame { 237 | internalScrollView.setContentOffset(CGPoint(x: frame.midX - (internalScrollView.frame.width / 2), y: 0), animated: true) 238 | } 239 | } 240 | 241 | public func reloadData() { 242 | recreateCacheFromDataSource() 243 | updateScroll() 244 | recenterIfNeeded(allowThrottle: false, allowShiftTileViews: false) 245 | tileItemViewsIfNeeded(allowThrottle: false, targetContentOffsetX: nil) 246 | setupSingleItemIfNeeded() 247 | } 248 | 249 | /// The whole width of scrollable area. Should be big enough for user not to "bounce" against it. 250 | /// Offset will be resetted within it as often as possible, so that user constantly see only ceter-ish section of it. 251 | /// 252 | /// Recentering is disabled during animations. 253 | /// Consider increasing this value if user swipes vilently and device screen is wide. 254 | /// Suggestion: 10 x Screen.width 255 | /// It is better to call `reloadData()` if new value is set. 256 | /// 257 | /// Default value is 5000. 258 | public func setScrollableContentWidth(_ newValue: CGFloat) { 259 | scrollableContentWidth = newValue 260 | setNeedsLayout() 261 | } 262 | 263 | /// See singleItemBehavior for description. 264 | /// Reloading of data is required after changing this setting. 265 | public func setSingleItemBehavior(_ newValue: SingleItemBehavior) { 266 | singleItemBehavior = newValue 267 | internalScrollView.bounces = newValue == .bounce 268 | internalScrollView.alwaysBounceHorizontal = newValue == .bounce 269 | setNeedsLayout() 270 | } 271 | 272 | public override func setNeedsLayout() { 273 | lastLayoutBoundsSize = .zero 274 | super.setNeedsLayout() 275 | } 276 | 277 | /// Index of item, which view is intersecting central line of drawing area. 278 | /// 279 | /// - returns: `nil` if there are no items 280 | public func indexOfItemAtVisibleCenter() -> Int? { 281 | return nearestVisibleCenterItem(targetOffsetX: internalScrollView.contentOffset.x)?.itemIndex 282 | } 283 | 284 | } 285 | 286 | // MARK: - UIScrollViewDelegate 287 | 288 | extension InfinityScrollView: UIScrollViewDelegate { 289 | 290 | public func scrollViewDidScroll(_ scrollView: UIScrollView) { 291 | guard scrollView === internalScrollView else { 292 | return 293 | } 294 | tileItemViewsIfNeeded(allowThrottle: true, targetContentOffsetX: nil) 295 | } 296 | 297 | public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { 298 | guard scrollView === internalScrollView else { 299 | return 300 | } 301 | let visibleItem = nearestVisibleCenterItem(targetOffsetX: scrollView.contentOffset.x) 302 | dragStartedTileIndex = visibleItem?.tileIndex 303 | if decelerationAnimationData != nil { 304 | if isDecelerationEnabled { 305 | if (cachedNumberOfItems > 1) || (singleItemBehavior == .tile) || isSnapEnabled { 306 | switch snapAnimation { 307 | case .none: 308 | // already reported 309 | break 310 | case .scrollView: 311 | // deceleration animation from UIScrollView is used 312 | // will report in `scrollViewDidEndDecelerating()` 313 | if let itemIndex = visibleItem?.itemIndex { 314 | delegate?.infinityScrollView(self, didEndDeceleratingOnItemAtIndex: itemIndex, wasAborted: true) 315 | } 316 | case .curve, .spring: 317 | // will be reported in `animationDidStop(...)` 318 | break 319 | } 320 | } else { 321 | // special case for single item: non-scrollView animation do not work 322 | // : deceleration animation from UIScrollView is used 323 | // : will report in `scrollViewDidEndDecelerating()` 324 | if let itemIndex = visibleItem?.itemIndex { 325 | delegate?.infinityScrollView(self, didEndDeceleratingOnItemAtIndex: itemIndex, wasAborted: true) 326 | } 327 | } 328 | } else { 329 | // already reported 330 | } 331 | } else { 332 | // already reported 333 | } 334 | stopSnapAnimation() 335 | decelerationAnimationData = nil // beginning of next drag cycle 336 | recenterIfNeeded(allowThrottle: true, allowShiftTileViews: true) 337 | } 338 | 339 | public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { 340 | guard scrollView === internalScrollView else { 341 | return 342 | } 343 | if let decelerationData = decelerationAnimationData { 344 | if isDecelerationEnabled { 345 | if (cachedNumberOfItems > 1) || (singleItemBehavior == .tile) || isSnapEnabled { 346 | switch snapAnimation { 347 | case .none: 348 | // no animation 349 | internalScrollView.bounds.origin.x = decelerationData.projectedContentOffsetX 350 | // report 351 | delegate?.infinityScrollView(self, 352 | didEndDeceleratingOnItemAtIndex: decelerationData.nearestItemData.itemIndex, 353 | wasAborted: false) 354 | case .scrollView: 355 | // deceleration animation from UIScrollView is used 356 | // will report in `scrollViewDidEndDecelerating(...)` or `scrollViewWillBeginDragging(...)` 357 | break 358 | case .curve, .spring: 359 | startSnapAnimation(initialVelocityX: decelerationData.initialVelocityX, 360 | targetContentOffset: CGPoint(x: decelerationData.projectedContentOffsetX, y: 0)) 361 | } 362 | } else { 363 | // fallback to 'scrollView' behaviour - other animations make no sense and do not work 364 | // : deceleration animation from UIScrollView is used 365 | // : will report in `scrollViewDidEndDecelerating(...)` or `scrollViewWillBeginDragging(...)` 366 | } 367 | } else { 368 | delegate?.infinityScrollView(self, didEndDeceleratingOnItemAtIndex: decelerationData.nearestItemData.itemIndex, wasAborted: false) 369 | } 370 | } else { 371 | // there are no items - nothing to work with 372 | } 373 | } 374 | 375 | public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { 376 | guard scrollView === internalScrollView else { 377 | return 378 | } 379 | if decelerationAnimationData != nil { 380 | if isDecelerationEnabled { 381 | if (cachedNumberOfItems > 1) || (singleItemBehavior == .tile) || isSnapEnabled { 382 | switch snapAnimation { 383 | case .none: 384 | // already reported 385 | break 386 | case .scrollView: 387 | if let itemIndex = nearestVisibleCenterItem(targetOffsetX: scrollView.contentOffset.x)?.itemIndex { 388 | delegate?.infinityScrollView(self, didEndDeceleratingOnItemAtIndex: itemIndex, wasAborted: false) 389 | } 390 | decelerationAnimationData = nil 391 | case .curve, .spring: 392 | // will be reported in `animationDidStop(...)` 393 | break 394 | } 395 | } else { 396 | // special case for single item behaviour: non-scrollView animations do not work 397 | if let itemIndex = nearestVisibleCenterItem(targetOffsetX: scrollView.contentOffset.x)?.itemIndex { 398 | // in case of bounce: scrollViewDidEndDecelerating will be called before scrollViewWillBeginDragging 399 | // so, if scrollView.isTracking -> deceleration (bounce) was aborted 400 | delegate?.infinityScrollView(self, didEndDeceleratingOnItemAtIndex: itemIndex, wasAborted: scrollView.isTracking) 401 | } 402 | decelerationAnimationData = nil 403 | } 404 | } else { 405 | // already reported 406 | } 407 | } else { 408 | // already reported 409 | } 410 | } 411 | 412 | public func scrollViewWillEndDragging(_ scrollView: UIScrollView, 413 | withVelocity velocity: CGPoint, 414 | targetContentOffset: UnsafeMutablePointer) { 415 | guard scrollView === internalScrollView else { 416 | return 417 | } 418 | var projectedContentOffsetX = scrollView.contentOffset.x 419 | if isDecelerationEnabled { 420 | projectedContentOffsetX = DecelerationHelper.project(value: scrollView.contentOffset.x, 421 | initialVelocity: velocity.x, 422 | decelerationRate: scrollView.decelerationRate.rawValue) 423 | tileItemViewsIfNeeded(allowThrottle: false, targetContentOffsetX: projectedContentOffsetX) 424 | } 425 | 426 | if let nearestItemData = nearestVisibleCenterItem(targetOffsetX: projectedContentOffsetX) { 427 | if let startIndex = dragStartedTileIndex { 428 | let direction = swipeDirection(startIndex: startIndex, endIndex: nearestItemData.tileIndex) 429 | delegate?.infinityScrollView(self, willEndSwipeOnItemAtIndex: nearestItemData.itemIndex, swipeDirection: direction) 430 | } 431 | if isSnapEnabled { 432 | projectedContentOffsetX = nearestItemData.anchorOffsetX 433 | } 434 | decelerationAnimationData = AnimationData(projectedContentOffsetX: projectedContentOffsetX, 435 | nearestItemData: nearestItemData, 436 | initialVelocityX: velocity.x) 437 | if (cachedNumberOfItems > 1) || (singleItemBehavior == .tile) || isSnapEnabled { 438 | switch snapAnimation { 439 | case .none: 440 | targetContentOffset.pointee = scrollView.contentOffset // stop system animation 441 | case .scrollView: 442 | targetContentOffset.pointee = CGPoint(x: projectedContentOffsetX, y: 0) 443 | case .curve, .spring: 444 | targetContentOffset.pointee = scrollView.contentOffset // stop system animation 445 | allowTiling -= 1 446 | allowRecenter -= 1 447 | } 448 | } else { 449 | // without snap non-scrollview animations make no sense for non-tiled single item -> fallback to scrollView 450 | targetContentOffset.pointee = CGPoint(x: projectedContentOffsetX, y: 0) 451 | } 452 | } 453 | } 454 | 455 | } 456 | 457 | // MARK: - CAAnimationDelegate 458 | 459 | extension InfinityScrollView: CAAnimationDelegate { 460 | 461 | public func animationDidStop(_ anim: CAAnimation, finished finishedFlag: Bool) { 462 | if let animationName = anim.value(forKey: Constant.snapAnimationNameKey) as? String, 463 | animationName == Constant.snapAnimationNameValue { 464 | allowRecenter += 1 465 | allowTiling += 1 466 | decelerateAnimation?.delegate = nil 467 | decelerateAnimation = nil 468 | if isDecelerationEnabled { 469 | switch snapAnimation { 470 | case .none: 471 | // already reported 472 | break 473 | case .scrollView: 474 | // will be reported in `scrollViewDidEndDecelerating(...)` or `scrollViewDidEndDragging(...)` 475 | break 476 | case .curve, .spring: 477 | if let presentationLayer = internalScrollView.layer.presentation(), 478 | let nearestItem = nearestVisibleCenterItem(targetOffsetX: presentationLayer.bounds.origin.x) { 479 | delegate?.infinityScrollView(self, 480 | didEndDeceleratingOnItemAtIndex: nearestItem.itemIndex, 481 | wasAborted: !finishedFlag) 482 | } else { 483 | // nothing to report about 484 | } 485 | } 486 | } else { 487 | // already reported 488 | } 489 | } 490 | } 491 | 492 | } 493 | 494 | // MARK: - UIGestureRecognizerDelegate 495 | 496 | extension InfinityScrollView: UIGestureRecognizerDelegate { 497 | 498 | public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { 499 | switch gestureRecognizer { 500 | case touchDownRecognizer: 501 | return internalScrollView.layer.animation(forKey: Constant.snapAnimationKey) != nil 502 | case tapRecognizer: 503 | return (internalScrollView.layer.animation(forKey: Constant.snapAnimationKey) == nil) && !internalScrollView.isDecelerating 504 | default: 505 | return super.gestureRecognizerShouldBegin(gestureRecognizer) 506 | } 507 | } 508 | 509 | public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, 510 | shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { 511 | switch gestureRecognizer { 512 | case touchDownRecognizer, 513 | tapRecognizer: 514 | return true 515 | default: 516 | return false 517 | } 518 | } 519 | 520 | public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, 521 | shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool { 522 | if (gestureRecognizer === tapRecognizer) && (otherGestureRecognizer === touchDownRecognizer) { 523 | return true 524 | } else { 525 | return false 526 | } 527 | } 528 | 529 | public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, 530 | shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool { 531 | if (gestureRecognizer === touchDownRecognizer) && (otherGestureRecognizer === touchDownRecognizer) { 532 | return true 533 | } else { 534 | return false 535 | } 536 | } 537 | 538 | } 539 | 540 | // MARK: - Private 541 | 542 | private extension InfinityScrollView { 543 | 544 | private func commonInit() { 545 | let scrollView = UIScrollView(frame: bounds) 546 | scrollView.showsVerticalScrollIndicator = false 547 | scrollView.showsHorizontalScrollIndicator = false 548 | scrollView.decelerationRate = UIScrollView.DecelerationRate.normal 549 | scrollView.scrollsToTop = false 550 | scrollView.delegate = self 551 | scrollView.backgroundColor = UIColor.clear 552 | scrollView.contentSize = CGSize(width: scrollableContentWidth, height: bounds.height) 553 | scrollView.translatesAutoresizingMaskIntoConstraints = true // manual layout by frame 554 | addSubview(scrollView) 555 | internalScrollView = scrollView 556 | 557 | let containerView = UIView(frame: CGRect(x: 0, y: 0, width: scrollableContentWidth, height: bounds.height)) 558 | containerView.backgroundColor = UIColor.clear 559 | containerView.translatesAutoresizingMaskIntoConstraints = true // manual layout by frame 560 | scrollView.addSubview(containerView) 561 | contentContainerView = containerView 562 | 563 | let localTouchDownRecognizer = SingleTouchDownGestureRecognizer(target: self, action: #selector(handleSingleTouchDownGestureRecognized(_:))) 564 | localTouchDownRecognizer.delegate = self 565 | internalScrollView.addGestureRecognizer(localTouchDownRecognizer) 566 | touchDownRecognizer = localTouchDownRecognizer 567 | 568 | let localTapRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapGestureRecognized(_:))) 569 | localTapRecognizer.delegate = self 570 | addGestureRecognizer(localTapRecognizer) 571 | tapRecognizer = localTapRecognizer 572 | } 573 | 574 | private func recreateCacheFromDataSource() { 575 | visibleTileViews.forEach({ $0.value.removeFromSuperview() }) 576 | visibleTileViews.removeAll() 577 | tileDoneForContentOffsetX = .infinity 578 | cachedNumberOfItems = 0 579 | cachedItemWidths.removeAll() 580 | cachedItemsTotalWidth = 0 581 | cachedZeroItemOffset = 0 582 | recenteredZeroItemOffset = 0 583 | guard let strongDataSource = dataSource else { 584 | return 585 | } 586 | cachedNumberOfItems = strongDataSource.infinityScrollViewNumberOfItems(self) 587 | assert(cachedNumberOfItems >= 0, "number of items can't be negative") 588 | guard cachedNumberOfItems > 0 else { 589 | return 590 | } 591 | for itemIndex in 0.. 0, 608 | cachedNumberOfItems > 0, 609 | (cachedNumberOfItems > 1 || singleItemBehavior == .tile), 610 | let strongDataSource = dataSource else { 611 | return 612 | } 613 | 614 | let contentOffsetDistance = abs(tileDoneForContentOffsetX - internalScrollView.contentOffset.x) 615 | guard !allowThrottle || contentOffsetDistance > tileThrottleDistance else { 616 | return 617 | } 618 | 619 | let visibleBounds = internalScrollView.bounds 620 | let minVisibleX: CGFloat 621 | let maxVisibleX: CGFloat 622 | if let targetX = targetContentOffsetX { 623 | minVisibleX = min(visibleBounds.minX, targetX) - visibleAreaOverhangX 624 | maxVisibleX = max(visibleBounds.maxX, targetX) + visibleAreaOverhangX 625 | } else { 626 | minVisibleX = visibleBounds.minX - visibleAreaOverhangX 627 | maxVisibleX = visibleBounds.maxX + visibleAreaOverhangX 628 | } 629 | 630 | // remove existing tiles, that are no longer visible 631 | for (index, tileView) in visibleTileViews { 632 | if (tileView.frame.minX > maxVisibleX) || (tileView.frame.maxX < minVisibleX) { 633 | visibleTileViews.removeValue(forKey: index) 634 | tileView.removeFromSuperview() 635 | } 636 | } 637 | 638 | // prepare for adding new tiles 639 | let zeroItemStartX = scrollableContentWidth / 2.0 + cachedZeroItemOffset + recenteredZeroItemOffset 640 | let iterationMultiplier = floor((minVisibleX - zeroItemStartX) / cachedItemsTotalWidth) 641 | let tileStartX = zeroItemStartX + iterationMultiplier * cachedItemsTotalWidth 642 | 643 | // skip non-visible tiles (to the left of minVisibleX) 644 | var currentTileX = tileStartX 645 | var currentTileIndex: Int = 0 + Int(iterationMultiplier) * cachedNumberOfItems 646 | var currentItemWidth = cachedItemWidths[itemIndex(tileIndex: currentTileIndex)] 647 | var currentTileMaxX = currentTileX + currentItemWidth 648 | while currentTileMaxX < minVisibleX { 649 | currentTileIndex += 1 650 | currentItemWidth = cachedItemWidths[itemIndex(tileIndex: currentTileIndex)] 651 | currentTileX = currentTileMaxX 652 | currentTileMaxX += currentItemWidth 653 | } 654 | 655 | // add new tiles 656 | let itemHeight = visibleBounds.height 657 | while currentTileX < maxVisibleX { 658 | let currentItemIndex = itemIndex(tileIndex: currentTileIndex) 659 | currentItemWidth = cachedItemWidths[currentItemIndex] 660 | if visibleTileViews[currentTileIndex] == nil { 661 | let newTileFrame = CGRect(x: currentTileX, y: 0, width: currentItemWidth, height: itemHeight) 662 | let newTileView = strongDataSource.infinityScrollView(self, viewForItemAtIndex: currentItemIndex, size: newTileFrame.size) 663 | newTileView.frame = newTileFrame 664 | contentContainerView.addSubview(newTileView) 665 | visibleTileViews[currentTileIndex] = newTileView 666 | } 667 | currentTileX += currentItemWidth 668 | currentTileIndex += 1 669 | } 670 | 671 | tileDoneForContentOffsetX = internalScrollView.contentOffset.x 672 | } 673 | 674 | private func itemIndex(tileIndex: Int) -> Int { 675 | var result = tileIndex % cachedNumberOfItems 676 | if result < 0 { 677 | result += cachedNumberOfItems 678 | } 679 | return result 680 | } 681 | 682 | /// Move content so that center of content align with visual center of InfinityScrollView. 683 | /// - parameter allowThrottle: if true - recentering will be skipped, if since last time content offset was changed too little. 684 | /// - parameter allowShiftTileViews: if true - tile views will be shifted in the opposite of 'recenter' direction to keep them visually in place. 685 | private func recenterIfNeeded(allowThrottle: Bool, allowShiftTileViews: Bool) { 686 | guard allowRecenter > 0, (cachedNumberOfItems > 1 || singleItemBehavior == .tile) else { 687 | return 688 | } 689 | 690 | let currentOffsetX = internalScrollView.contentOffset.x 691 | let centerOffsetX = (internalScrollView.contentSize.width - internalScrollView.bounds.size.width) / 2.0 692 | let shiftX = centerOffsetX - currentOffsetX 693 | if !allowThrottle || abs(shiftX) > recenterThrottleDistance { 694 | allowTiling -= 1 695 | internalScrollView.contentOffset = CGPoint(x: centerOffsetX, y: 0) 696 | tileDoneForContentOffsetX += shiftX // sync throttling for tiling 697 | if allowShiftTileViews { 698 | for (_, tileView) in visibleTileViews { 699 | var tileFrame = tileView.frame 700 | tileFrame.origin.x += shiftX 701 | tileView.frame = tileFrame 702 | } 703 | recenteredZeroItemOffset += shiftX 704 | } 705 | allowTiling += 1 706 | } 707 | } 708 | 709 | private func updateVisibleTileViews(height newHeight: CGFloat) { 710 | visibleTileViews.forEach({ (_, tileView) in 711 | tileView.frame.size.height = newHeight 712 | }) 713 | } 714 | 715 | /// Update scrollable area 716 | private func updateScroll() { 717 | internalScrollView.frame = bounds 718 | let contentWidth: CGFloat 719 | if cachedNumberOfItems == 1, singleItemBehavior != .tile { 720 | contentWidth = max(bounds.width, cachedItemsTotalWidth) 721 | } else { 722 | contentWidth = scrollableContentWidth 723 | } 724 | internalScrollView.contentSize = CGSize(width: contentWidth, height: bounds.height) 725 | contentContainerView.frame = CGRect(x: 0, y: 0, width: contentWidth, height: bounds.height) 726 | } 727 | 728 | /// Special case for single item in data source when tiling is not allowed 729 | private func setupSingleItemIfNeeded() { 730 | guard cachedNumberOfItems == 1, singleItemBehavior != .tile, let strongDataSource = dataSource else { 731 | return 732 | } 733 | // add single tile 734 | let contentSize = contentContainerView.bounds.size 735 | let itemIndex: Int = 0 736 | let itemWidth = cachedItemWidths[itemIndex] 737 | let tileView: UIView 738 | if let oldTileView = visibleTileViews[itemIndex] { 739 | tileView = oldTileView 740 | } else { 741 | let newTileFrame = CGRect(x: 0, y: 0, width: itemWidth, height: contentSize.height) 742 | let newTileView = strongDataSource.infinityScrollView(self, viewForItemAtIndex: itemIndex, size: newTileFrame.size) 743 | newTileView.frame = newTileFrame 744 | contentContainerView.addSubview(newTileView) 745 | visibleTileViews[itemIndex] = newTileView 746 | tileView = newTileView 747 | } 748 | tileView.frame = CGRect(x: (contentSize.width - itemWidth) / 2.0, 749 | y: 0, 750 | width: itemWidth, 751 | height: contentSize.height) 752 | } 753 | 754 | /// this function assumes, that views for items was already tiled in advance. 755 | /// 756 | /// - parameter targetOffsetX: offset for internal scroll view to look nearst item around. 757 | /// - returns: 758 | /// `nil` if there are no items. 759 | /// Descriptive data about nearest item/tile. 760 | /// We are aiming to place center of item to the center of visible area. 761 | private func nearestVisibleCenterItem(targetOffsetX: CGFloat) -> NearestVisibleCenterItemData? { 762 | let screenHalfWidth = bounds.width / 2.0 763 | let targetCenterX = targetOffsetX + screenHalfWidth 764 | var nearestTile: Dictionary.Element? 765 | var nearestDistance: CGFloat = .infinity 766 | for tile in visibleTileViews { 767 | let distance = abs(tile.value.frame.origin.x + (tile.value.frame.size.width / 2.0) - targetCenterX) 768 | if distance < nearestDistance { 769 | nearestTile = tile 770 | nearestDistance = distance 771 | } 772 | } 773 | 774 | guard let foundElement = nearestTile else { 775 | return nil 776 | } 777 | 778 | let tileCenterX = foundElement.value.frame.origin.x + foundElement.value.frame.size.width / 2.0 779 | return NearestVisibleCenterItemData(anchorOffsetX: tileCenterX - screenHalfWidth , 780 | tileCenterX: tileCenterX, 781 | itemIndex: itemIndex(tileIndex: foundElement.key), 782 | tileIndex: foundElement.key) 783 | } 784 | 785 | private func startSnapAnimation(initialVelocityX: CGFloat, targetContentOffset: CGPoint) { 786 | stopSnapAnimation() 787 | 788 | let fromBounds = internalScrollView.bounds 789 | let toBounds = CGRect(x: targetContentOffset.x, y: targetContentOffset.y, width: fromBounds.width, height: fromBounds.height) 790 | 791 | switch snapAnimation { 792 | case .none, 793 | .scrollView: 794 | // should not be here - these two settings handled by `scrollViewWillEndDragging(:withVelocity:targetContentOffset:)` 795 | stopSnapAnimation() 796 | 797 | case .curve(let duration, let name): 798 | let animation = CABasicAnimation(keyPath: "bounds") 799 | animation.duration = duration 800 | animation.timingFunction = CAMediaTimingFunction(name: name) 801 | animation.fromValue = NSValue(cgRect: fromBounds) 802 | animation.toValue = NSValue(cgRect: toBounds) 803 | animation.isRemovedOnCompletion = true 804 | animation.delegate = self 805 | animation.setValue(Constant.snapAnimationNameValue, forKey: Constant.snapAnimationNameKey) 806 | decelerateAnimation = animation 807 | internalScrollView.layer.bounds = toBounds 808 | internalScrollView.layer.add(animation, forKey: Constant.snapAnimationKey) 809 | 810 | case .spring(let mass, let stiffness, let damping): 811 | let animation = CASpringAnimation(keyPath: "bounds") 812 | animation.mass = mass 813 | animation.stiffness = stiffness 814 | animation.damping = damping 815 | animation.initialVelocity = abs(initialVelocityX) 816 | animation.fromValue = NSValue(cgRect: fromBounds) 817 | animation.toValue = NSValue(cgRect: toBounds) 818 | animation.duration = animation.settlingDuration 819 | animation.isRemovedOnCompletion = true 820 | animation.delegate = self 821 | animation.setValue(Constant.snapAnimationNameValue, forKey: Constant.snapAnimationNameKey) 822 | decelerateAnimation = animation 823 | internalScrollView.layer.bounds = toBounds 824 | internalScrollView.layer.add(animation, forKey: Constant.snapAnimationKey) 825 | } 826 | } 827 | 828 | private func stopSnapAnimation() { 829 | guard internalScrollView.layer.animation(forKey: Constant.snapAnimationKey) != nil else { 830 | return 831 | } 832 | if let presentationLayer = internalScrollView.layer.presentation() { 833 | internalScrollView.layer.bounds = presentationLayer.bounds 834 | // NOTE: There is a slight but noticeable glitch: 835 | // - next frame is rendered as if animation is still going 836 | // - frame after next is rendered with "stopped" bounds 837 | } 838 | internalScrollView.layer.removeAnimation(forKey: Constant.snapAnimationKey) 839 | } 840 | 841 | private func swipeDirection(startIndex: Int, endIndex: Int) -> SwipeDirection { 842 | if startIndex == endIndex { 843 | return .none 844 | } else if startIndex > endIndex { 845 | return .left 846 | } else { 847 | return .right 848 | } 849 | } 850 | 851 | @objc private func handleSingleTouchDownGestureRecognized(_ gestureRecognizer: UIGestureRecognizer) { 852 | stopSnapAnimation() 853 | } 854 | 855 | @objc private func handleTapGestureRecognized(_ gestureRecognizer: UIGestureRecognizer) { 856 | let point = gestureRecognizer.location(in: internalScrollView) 857 | // not using `nearestVisibleCenterItem()` here: that function is heavier and adjusts for center of visible area 858 | var nearestTile: Dictionary.Element? 859 | var nearestDistance: CGFloat = .infinity 860 | for tile in visibleTileViews { 861 | let distance = abs(tile.value.frame.origin.x + (tile.value.frame.size.width / 2.0) - point.x) 862 | if distance < nearestDistance { 863 | nearestTile = tile 864 | nearestDistance = distance 865 | } 866 | } 867 | guard let foundElement = nearestTile else { 868 | return 869 | } 870 | let index = itemIndex(tileIndex: foundElement.key) 871 | delegate?.infinityScrollView(self, didSelectItemAtIndex: index) 872 | } 873 | 874 | } 875 | -------------------------------------------------------------------------------- /Source/SingleTouchDownGestureRecognizer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // All credits go to: https://stackoverflow.com/a/15629234 3 | // 4 | 5 | import Foundation 6 | import UIKit 7 | 8 | open class SingleTouchDownGestureRecognizer: UIGestureRecognizer { 9 | 10 | open override func touchesBegan(_ touches: Set, with event: UIEvent) { 11 | if state == .possible { 12 | state = .recognized 13 | } 14 | } 15 | 16 | open override func touchesMoved(_ touches: Set, with event: UIEvent) { 17 | state = .failed 18 | } 19 | 20 | open override func touchesEnded(_ touches: Set, with event: UIEvent) { 21 | state = .failed 22 | } 23 | 24 | } 25 | --------------------------------------------------------------------------------