├── .gitignore ├── CustomPaging.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── CustomPaging.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── CustomPaging ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ ├── collection_tab_bar_icon.imageset │ │ ├── Contents.json │ │ └── collection_tab_bar_icon.pdf │ └── table_tab_bar_icon.imageset │ │ ├── Contents.json │ │ └── table_tab_bar_icon.pdf ├── Base.lproj │ └── LaunchScreen.storyboard ├── Info.plist └── Sources │ ├── AppDelegate.swift │ ├── CGPoint+Utils.swift │ ├── Clamp.swift │ ├── Collection │ ├── CollectionCell.swift │ ├── CollectionSettingsView.swift │ ├── CollectionViewController.swift │ └── SliderView.swift │ ├── PagingView.swift │ ├── Projection.swift │ ├── Table │ ├── CellInfo.swift │ ├── TableCellInfo.swift │ └── TableViewController.swift │ ├── UIColor+Application.swift │ └── UIColor+Utils.swift ├── Podfile ├── Podfile.lock └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | # Package.resolved 41 | .build/ 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | Pods/ 50 | 51 | # Carthage 52 | # 53 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 54 | # Carthage/Checkouts 55 | 56 | Carthage/Build 57 | 58 | # fastlane 59 | # 60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 61 | # screenshots whenever they are needed. 62 | # For more information about the recommended setup visit: 63 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 64 | 65 | fastlane/report.xml 66 | fastlane/Preview.html 67 | fastlane/screenshots/**/*.png 68 | fastlane/test_output 69 | -------------------------------------------------------------------------------- /CustomPaging.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 66CCF0B9E4CBBD23D49AEAD0 /* Pods_CustomPaging.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A4150B38C019AF79924FCA98 /* Pods_CustomPaging.framework */; }; 11 | B1101093211511D50050F17F /* TableCellInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1101092211511D50050F17F /* TableCellInfo.swift */; }; 12 | B1376953211505B30016FDC4 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1376952211505B30016FDC4 /* AppDelegate.swift */; }; 13 | B1376955211505B30016FDC4 /* TableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1376954211505B30016FDC4 /* TableViewController.swift */; }; 14 | B137695A211505B40016FDC4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B1376959211505B40016FDC4 /* Assets.xcassets */; }; 15 | B137695D211505B40016FDC4 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B137695B211505B40016FDC4 /* LaunchScreen.storyboard */; }; 16 | B1C94E0621150AB900EAC3CE /* UIColor+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1C94E0521150AB900EAC3CE /* UIColor+Utils.swift */; }; 17 | B1E051022132E35300A710D2 /* CollectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1E051012132E35300A710D2 /* CollectionCell.swift */; }; 18 | B1E051042132EC2900A710D2 /* PagingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1E051032132EC2900A710D2 /* PagingView.swift */; }; 19 | B1E05106213340F000A710D2 /* CGPoint+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1E05105213340F000A710D2 /* CGPoint+Utils.swift */; }; 20 | B1E0510821334D1700A710D2 /* SliderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1E0510721334D1700A710D2 /* SliderView.swift */; }; 21 | B1E0510A21336DC400A710D2 /* UIColor+Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1E0510921336DC400A710D2 /* UIColor+Application.swift */; }; 22 | B1E0510C21336ECE00A710D2 /* CollectionSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1E0510B21336ECE00A710D2 /* CollectionSettingsView.swift */; }; 23 | B1E0510E2133775400A710D2 /* Clamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1E0510D2133775400A710D2 /* Clamp.swift */; }; 24 | B1F52EDA2130CE5700E8ECB1 /* Projection.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1F52ED92130CE5700E8ECB1 /* Projection.swift */; }; 25 | B1F52EE02131B44D00E8ECB1 /* CollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1F52EDF2131B44D00E8ECB1 /* CollectionViewController.swift */; }; 26 | /* End PBXBuildFile section */ 27 | 28 | /* Begin PBXFileReference section */ 29 | 70F0E55BCF84C5991887BB3C /* Pods-CustomPaging.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CustomPaging.release.xcconfig"; path = "Pods/Target Support Files/Pods-CustomPaging/Pods-CustomPaging.release.xcconfig"; sourceTree = ""; }; 30 | A4150B38C019AF79924FCA98 /* Pods_CustomPaging.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_CustomPaging.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 31 | B1101092211511D50050F17F /* TableCellInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableCellInfo.swift; sourceTree = ""; }; 32 | B137694F211505B30016FDC4 /* CustomPaging.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CustomPaging.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33 | B1376952211505B30016FDC4 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 34 | B1376954211505B30016FDC4 /* TableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewController.swift; sourceTree = ""; }; 35 | B1376959211505B40016FDC4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 36 | B137695C211505B40016FDC4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 37 | B137695E211505B40016FDC4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 38 | B1C94E0521150AB900EAC3CE /* UIColor+Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Utils.swift"; sourceTree = ""; }; 39 | B1E051012132E35300A710D2 /* CollectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionCell.swift; sourceTree = ""; }; 40 | B1E051032132EC2900A710D2 /* PagingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagingView.swift; sourceTree = ""; }; 41 | B1E05105213340F000A710D2 /* CGPoint+Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGPoint+Utils.swift"; sourceTree = ""; }; 42 | B1E0510721334D1700A710D2 /* SliderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SliderView.swift; sourceTree = ""; }; 43 | B1E0510921336DC400A710D2 /* UIColor+Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Application.swift"; sourceTree = ""; }; 44 | B1E0510B21336ECE00A710D2 /* CollectionSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionSettingsView.swift; sourceTree = ""; }; 45 | B1E0510D2133775400A710D2 /* Clamp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Clamp.swift; sourceTree = ""; }; 46 | B1F52ED92130CE5700E8ECB1 /* Projection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Projection.swift; sourceTree = ""; }; 47 | B1F52EDF2131B44D00E8ECB1 /* CollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewController.swift; sourceTree = ""; }; 48 | CC7DF618252C0C773E166A50 /* Pods-CustomPaging.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CustomPaging.debug.xcconfig"; path = "Pods/Target Support Files/Pods-CustomPaging/Pods-CustomPaging.debug.xcconfig"; sourceTree = ""; }; 49 | /* End PBXFileReference section */ 50 | 51 | /* Begin PBXFrameworksBuildPhase section */ 52 | B137694C211505B30016FDC4 /* Frameworks */ = { 53 | isa = PBXFrameworksBuildPhase; 54 | buildActionMask = 2147483647; 55 | files = ( 56 | 66CCF0B9E4CBBD23D49AEAD0 /* Pods_CustomPaging.framework in Frameworks */, 57 | ); 58 | runOnlyForDeploymentPostprocessing = 0; 59 | }; 60 | /* End PBXFrameworksBuildPhase section */ 61 | 62 | /* Begin PBXGroup section */ 63 | B1376946211505B30016FDC4 = { 64 | isa = PBXGroup; 65 | children = ( 66 | B1376951211505B30016FDC4 /* CustomPaging */, 67 | B1376950211505B30016FDC4 /* Products */, 68 | DFCA4256406026D271828ECA /* Pods */, 69 | DC0F54AA7343C3E6672A6AA1 /* Frameworks */, 70 | ); 71 | sourceTree = ""; 72 | }; 73 | B1376950211505B30016FDC4 /* Products */ = { 74 | isa = PBXGroup; 75 | children = ( 76 | B137694F211505B30016FDC4 /* CustomPaging.app */, 77 | ); 78 | name = Products; 79 | sourceTree = ""; 80 | }; 81 | B1376951211505B30016FDC4 /* CustomPaging */ = { 82 | isa = PBXGroup; 83 | children = ( 84 | B1F52EDB2131B1F100E8ECB1 /* Sources */, 85 | B1376959211505B40016FDC4 /* Assets.xcassets */, 86 | B137695B211505B40016FDC4 /* LaunchScreen.storyboard */, 87 | B137695E211505B40016FDC4 /* Info.plist */, 88 | ); 89 | path = CustomPaging; 90 | sourceTree = ""; 91 | }; 92 | B1F52EDB2131B1F100E8ECB1 /* Sources */ = { 93 | isa = PBXGroup; 94 | children = ( 95 | B1F52EDE2131B43400E8ECB1 /* Collection */, 96 | B1F52EDC2131B20E00E8ECB1 /* Table */, 97 | B1376952211505B30016FDC4 /* AppDelegate.swift */, 98 | B1C94E0521150AB900EAC3CE /* UIColor+Utils.swift */, 99 | B1F52ED92130CE5700E8ECB1 /* Projection.swift */, 100 | B1E051032132EC2900A710D2 /* PagingView.swift */, 101 | B1E05105213340F000A710D2 /* CGPoint+Utils.swift */, 102 | B1E0510921336DC400A710D2 /* UIColor+Application.swift */, 103 | B1E0510D2133775400A710D2 /* Clamp.swift */, 104 | ); 105 | path = Sources; 106 | sourceTree = ""; 107 | }; 108 | B1F52EDC2131B20E00E8ECB1 /* Table */ = { 109 | isa = PBXGroup; 110 | children = ( 111 | B1376954211505B30016FDC4 /* TableViewController.swift */, 112 | B1101092211511D50050F17F /* TableCellInfo.swift */, 113 | ); 114 | path = Table; 115 | sourceTree = ""; 116 | }; 117 | B1F52EDE2131B43400E8ECB1 /* Collection */ = { 118 | isa = PBXGroup; 119 | children = ( 120 | B1F52EDF2131B44D00E8ECB1 /* CollectionViewController.swift */, 121 | B1E051012132E35300A710D2 /* CollectionCell.swift */, 122 | B1E0510721334D1700A710D2 /* SliderView.swift */, 123 | B1E0510B21336ECE00A710D2 /* CollectionSettingsView.swift */, 124 | ); 125 | path = Collection; 126 | sourceTree = ""; 127 | }; 128 | DC0F54AA7343C3E6672A6AA1 /* Frameworks */ = { 129 | isa = PBXGroup; 130 | children = ( 131 | A4150B38C019AF79924FCA98 /* Pods_CustomPaging.framework */, 132 | ); 133 | name = Frameworks; 134 | sourceTree = ""; 135 | }; 136 | DFCA4256406026D271828ECA /* Pods */ = { 137 | isa = PBXGroup; 138 | children = ( 139 | CC7DF618252C0C773E166A50 /* Pods-CustomPaging.debug.xcconfig */, 140 | 70F0E55BCF84C5991887BB3C /* Pods-CustomPaging.release.xcconfig */, 141 | ); 142 | name = Pods; 143 | sourceTree = ""; 144 | }; 145 | /* End PBXGroup section */ 146 | 147 | /* Begin PBXNativeTarget section */ 148 | B137694E211505B30016FDC4 /* CustomPaging */ = { 149 | isa = PBXNativeTarget; 150 | buildConfigurationList = B1376961211505B40016FDC4 /* Build configuration list for PBXNativeTarget "CustomPaging" */; 151 | buildPhases = ( 152 | A5C76D5D43ADC1E92E2E5BB8 /* [CP] Check Pods Manifest.lock */, 153 | B137694B211505B30016FDC4 /* Sources */, 154 | B137694C211505B30016FDC4 /* Frameworks */, 155 | B137694D211505B30016FDC4 /* Resources */, 156 | 561415E31F5770B279A0DEE9 /* [CP] Embed Pods Frameworks */, 157 | ); 158 | buildRules = ( 159 | ); 160 | dependencies = ( 161 | ); 162 | name = CustomPaging; 163 | productName = CustomPaging; 164 | productReference = B137694F211505B30016FDC4 /* CustomPaging.app */; 165 | productType = "com.apple.product-type.application"; 166 | }; 167 | /* End PBXNativeTarget section */ 168 | 169 | /* Begin PBXProject section */ 170 | B1376947211505B30016FDC4 /* Project object */ = { 171 | isa = PBXProject; 172 | attributes = { 173 | LastSwiftUpdateCheck = 0940; 174 | LastUpgradeCheck = 0940; 175 | ORGANIZATIONNAME = "Ilya Lobanov"; 176 | TargetAttributes = { 177 | B137694E211505B30016FDC4 = { 178 | CreatedOnToolsVersion = 9.4.1; 179 | }; 180 | }; 181 | }; 182 | buildConfigurationList = B137694A211505B30016FDC4 /* Build configuration list for PBXProject "CustomPaging" */; 183 | compatibilityVersion = "Xcode 9.3"; 184 | developmentRegion = en; 185 | hasScannedForEncodings = 0; 186 | knownRegions = ( 187 | en, 188 | Base, 189 | ); 190 | mainGroup = B1376946211505B30016FDC4; 191 | productRefGroup = B1376950211505B30016FDC4 /* Products */; 192 | projectDirPath = ""; 193 | projectRoot = ""; 194 | targets = ( 195 | B137694E211505B30016FDC4 /* CustomPaging */, 196 | ); 197 | }; 198 | /* End PBXProject section */ 199 | 200 | /* Begin PBXResourcesBuildPhase section */ 201 | B137694D211505B30016FDC4 /* Resources */ = { 202 | isa = PBXResourcesBuildPhase; 203 | buildActionMask = 2147483647; 204 | files = ( 205 | B137695D211505B40016FDC4 /* LaunchScreen.storyboard in Resources */, 206 | B137695A211505B40016FDC4 /* Assets.xcassets in Resources */, 207 | ); 208 | runOnlyForDeploymentPostprocessing = 0; 209 | }; 210 | /* End PBXResourcesBuildPhase section */ 211 | 212 | /* Begin PBXShellScriptBuildPhase section */ 213 | 561415E31F5770B279A0DEE9 /* [CP] Embed Pods Frameworks */ = { 214 | isa = PBXShellScriptBuildPhase; 215 | buildActionMask = 2147483647; 216 | files = ( 217 | ); 218 | inputPaths = ( 219 | "${SRCROOT}/Pods/Target Support Files/Pods-CustomPaging/Pods-CustomPaging-frameworks.sh", 220 | "${BUILT_PRODUCTS_DIR}/pop/pop.framework", 221 | ); 222 | name = "[CP] Embed Pods Frameworks"; 223 | outputPaths = ( 224 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/pop.framework", 225 | ); 226 | runOnlyForDeploymentPostprocessing = 0; 227 | shellPath = /bin/sh; 228 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-CustomPaging/Pods-CustomPaging-frameworks.sh\"\n"; 229 | showEnvVarsInLog = 0; 230 | }; 231 | A5C76D5D43ADC1E92E2E5BB8 /* [CP] Check Pods Manifest.lock */ = { 232 | isa = PBXShellScriptBuildPhase; 233 | buildActionMask = 2147483647; 234 | files = ( 235 | ); 236 | inputPaths = ( 237 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 238 | "${PODS_ROOT}/Manifest.lock", 239 | ); 240 | name = "[CP] Check Pods Manifest.lock"; 241 | outputPaths = ( 242 | "$(DERIVED_FILE_DIR)/Pods-CustomPaging-checkManifestLockResult.txt", 243 | ); 244 | runOnlyForDeploymentPostprocessing = 0; 245 | shellPath = /bin/sh; 246 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 247 | showEnvVarsInLog = 0; 248 | }; 249 | /* End PBXShellScriptBuildPhase section */ 250 | 251 | /* Begin PBXSourcesBuildPhase section */ 252 | B137694B211505B30016FDC4 /* Sources */ = { 253 | isa = PBXSourcesBuildPhase; 254 | buildActionMask = 2147483647; 255 | files = ( 256 | B1F52EE02131B44D00E8ECB1 /* CollectionViewController.swift in Sources */, 257 | B1C94E0621150AB900EAC3CE /* UIColor+Utils.swift in Sources */, 258 | B1101093211511D50050F17F /* TableCellInfo.swift in Sources */, 259 | B1E0510C21336ECE00A710D2 /* CollectionSettingsView.swift in Sources */, 260 | B1376955211505B30016FDC4 /* TableViewController.swift in Sources */, 261 | B1E051042132EC2900A710D2 /* PagingView.swift in Sources */, 262 | B1E05106213340F000A710D2 /* CGPoint+Utils.swift in Sources */, 263 | B1E051022132E35300A710D2 /* CollectionCell.swift in Sources */, 264 | B1F52EDA2130CE5700E8ECB1 /* Projection.swift in Sources */, 265 | B1376953211505B30016FDC4 /* AppDelegate.swift in Sources */, 266 | B1E0510A21336DC400A710D2 /* UIColor+Application.swift in Sources */, 267 | B1E0510821334D1700A710D2 /* SliderView.swift in Sources */, 268 | B1E0510E2133775400A710D2 /* Clamp.swift in Sources */, 269 | ); 270 | runOnlyForDeploymentPostprocessing = 0; 271 | }; 272 | /* End PBXSourcesBuildPhase section */ 273 | 274 | /* Begin PBXVariantGroup section */ 275 | B137695B211505B40016FDC4 /* LaunchScreen.storyboard */ = { 276 | isa = PBXVariantGroup; 277 | children = ( 278 | B137695C211505B40016FDC4 /* Base */, 279 | ); 280 | name = LaunchScreen.storyboard; 281 | sourceTree = ""; 282 | }; 283 | /* End PBXVariantGroup section */ 284 | 285 | /* Begin XCBuildConfiguration section */ 286 | B137695F211505B40016FDC4 /* Debug */ = { 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++14"; 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_RANGE_LOOP_ANALYSIS = YES; 313 | CLANG_WARN_STRICT_PROTOTYPES = YES; 314 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 315 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 316 | CLANG_WARN_UNREACHABLE_CODE = YES; 317 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 318 | CODE_SIGN_IDENTITY = "iPhone Developer"; 319 | COPY_PHASE_STRIP = NO; 320 | DEBUG_INFORMATION_FORMAT = dwarf; 321 | ENABLE_STRICT_OBJC_MSGSEND = YES; 322 | ENABLE_TESTABILITY = YES; 323 | GCC_C_LANGUAGE_STANDARD = gnu11; 324 | GCC_DYNAMIC_NO_PIC = NO; 325 | GCC_NO_COMMON_BLOCKS = YES; 326 | GCC_OPTIMIZATION_LEVEL = 0; 327 | GCC_PREPROCESSOR_DEFINITIONS = ( 328 | "DEBUG=1", 329 | "$(inherited)", 330 | ); 331 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 332 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 333 | GCC_WARN_UNDECLARED_SELECTOR = YES; 334 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 335 | GCC_WARN_UNUSED_FUNCTION = YES; 336 | GCC_WARN_UNUSED_VARIABLE = YES; 337 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 338 | MTL_ENABLE_DEBUG_INFO = YES; 339 | ONLY_ACTIVE_ARCH = YES; 340 | SDKROOT = iphoneos; 341 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 342 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 343 | }; 344 | name = Debug; 345 | }; 346 | B1376960211505B40016FDC4 /* Release */ = { 347 | isa = XCBuildConfiguration; 348 | buildSettings = { 349 | ALWAYS_SEARCH_USER_PATHS = NO; 350 | CLANG_ANALYZER_NONNULL = YES; 351 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 352 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 353 | CLANG_CXX_LIBRARY = "libc++"; 354 | CLANG_ENABLE_MODULES = YES; 355 | CLANG_ENABLE_OBJC_ARC = YES; 356 | CLANG_ENABLE_OBJC_WEAK = YES; 357 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 358 | CLANG_WARN_BOOL_CONVERSION = YES; 359 | CLANG_WARN_COMMA = YES; 360 | CLANG_WARN_CONSTANT_CONVERSION = YES; 361 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 362 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 363 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 364 | CLANG_WARN_EMPTY_BODY = YES; 365 | CLANG_WARN_ENUM_CONVERSION = YES; 366 | CLANG_WARN_INFINITE_RECURSION = YES; 367 | CLANG_WARN_INT_CONVERSION = YES; 368 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 369 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 370 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 371 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 372 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 373 | CLANG_WARN_STRICT_PROTOTYPES = YES; 374 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 375 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 376 | CLANG_WARN_UNREACHABLE_CODE = YES; 377 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 378 | CODE_SIGN_IDENTITY = "iPhone Developer"; 379 | COPY_PHASE_STRIP = NO; 380 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 381 | ENABLE_NS_ASSERTIONS = NO; 382 | ENABLE_STRICT_OBJC_MSGSEND = YES; 383 | GCC_C_LANGUAGE_STANDARD = gnu11; 384 | GCC_NO_COMMON_BLOCKS = YES; 385 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 386 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 387 | GCC_WARN_UNDECLARED_SELECTOR = YES; 388 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 389 | GCC_WARN_UNUSED_FUNCTION = YES; 390 | GCC_WARN_UNUSED_VARIABLE = YES; 391 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 392 | MTL_ENABLE_DEBUG_INFO = NO; 393 | SDKROOT = iphoneos; 394 | SWIFT_COMPILATION_MODE = wholemodule; 395 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 396 | VALIDATE_PRODUCT = YES; 397 | }; 398 | name = Release; 399 | }; 400 | B1376962211505B40016FDC4 /* Debug */ = { 401 | isa = XCBuildConfiguration; 402 | baseConfigurationReference = CC7DF618252C0C773E166A50 /* Pods-CustomPaging.debug.xcconfig */; 403 | buildSettings = { 404 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 405 | CODE_SIGN_STYLE = Automatic; 406 | DEVELOPMENT_TEAM = ""; 407 | INFOPLIST_FILE = CustomPaging/Info.plist; 408 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 409 | LD_RUNPATH_SEARCH_PATHS = ( 410 | "$(inherited)", 411 | "@executable_path/Frameworks", 412 | ); 413 | PRODUCT_BUNDLE_IDENTIFIER = "ru.ultra.custom-paging"; 414 | PRODUCT_NAME = "$(TARGET_NAME)"; 415 | SWIFT_VERSION = 4.2; 416 | TARGETED_DEVICE_FAMILY = "1,2"; 417 | }; 418 | name = Debug; 419 | }; 420 | B1376963211505B40016FDC4 /* Release */ = { 421 | isa = XCBuildConfiguration; 422 | baseConfigurationReference = 70F0E55BCF84C5991887BB3C /* Pods-CustomPaging.release.xcconfig */; 423 | buildSettings = { 424 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 425 | CODE_SIGN_STYLE = Automatic; 426 | DEVELOPMENT_TEAM = ""; 427 | INFOPLIST_FILE = CustomPaging/Info.plist; 428 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 429 | LD_RUNPATH_SEARCH_PATHS = ( 430 | "$(inherited)", 431 | "@executable_path/Frameworks", 432 | ); 433 | PRODUCT_BUNDLE_IDENTIFIER = "ru.ultra.custom-paging"; 434 | PRODUCT_NAME = "$(TARGET_NAME)"; 435 | SWIFT_VERSION = 4.2; 436 | TARGETED_DEVICE_FAMILY = "1,2"; 437 | }; 438 | name = Release; 439 | }; 440 | /* End XCBuildConfiguration section */ 441 | 442 | /* Begin XCConfigurationList section */ 443 | B137694A211505B30016FDC4 /* Build configuration list for PBXProject "CustomPaging" */ = { 444 | isa = XCConfigurationList; 445 | buildConfigurations = ( 446 | B137695F211505B40016FDC4 /* Debug */, 447 | B1376960211505B40016FDC4 /* Release */, 448 | ); 449 | defaultConfigurationIsVisible = 0; 450 | defaultConfigurationName = Release; 451 | }; 452 | B1376961211505B40016FDC4 /* Build configuration list for PBXNativeTarget "CustomPaging" */ = { 453 | isa = XCConfigurationList; 454 | buildConfigurations = ( 455 | B1376962211505B40016FDC4 /* Debug */, 456 | B1376963211505B40016FDC4 /* Release */, 457 | ); 458 | defaultConfigurationIsVisible = 0; 459 | defaultConfigurationName = Release; 460 | }; 461 | /* End XCConfigurationList section */ 462 | }; 463 | rootObject = B1376947211505B30016FDC4 /* Project object */; 464 | } 465 | -------------------------------------------------------------------------------- /CustomPaging.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CustomPaging.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /CustomPaging.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /CustomPaging.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /CustomPaging/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /CustomPaging/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /CustomPaging/Assets.xcassets/collection_tab_bar_icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "collection_tab_bar_icon.pdf", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /CustomPaging/Assets.xcassets/collection_tab_bar_icon.imageset/collection_tab_bar_icon.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/super-ultra/CustomPaging/c6a0ceeca5db81eb917bb08d5f93cf431f8d70c3/CustomPaging/Assets.xcassets/collection_tab_bar_icon.imageset/collection_tab_bar_icon.pdf -------------------------------------------------------------------------------- /CustomPaging/Assets.xcassets/table_tab_bar_icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "table_tab_bar_icon.pdf", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /CustomPaging/Assets.xcassets/table_tab_bar_icon.imageset/table_tab_bar_icon.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/super-ultra/CustomPaging/c6a0ceeca5db81eb917bb08d5f93cf431f8d70c3/CustomPaging/Assets.xcassets/table_tab_bar_icon.imageset/table_tab_bar_icon.pdf -------------------------------------------------------------------------------- /CustomPaging/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 | -------------------------------------------------------------------------------- /CustomPaging/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Custom Paging 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /CustomPaging/Sources/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // CustomPaging 4 | // 5 | // Created by Ilya Lobanov on 04/08/2018. 6 | // Copyright © 2018 Ilya Lobanov. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool 17 | { 18 | let tableController = AppDelegate.makeNavigationContoller(rootViewController: TableViewController(), 19 | title: "Table", image: UIImage(named: "table_tab_bar_icon")) 20 | 21 | let collectionController = AppDelegate.makeNavigationContoller(rootViewController: CollectionViewController(), 22 | title: "Collection", image: UIImage(named: "collection_tab_bar_icon")) 23 | 24 | let tabBarController = AppDelegate.makeTabBarController() 25 | tabBarController.viewControllers = [tableController, collectionController] 26 | 27 | window = UIWindow(frame: UIScreen.main.bounds) 28 | window?.rootViewController = tabBarController 29 | window?.makeKeyAndVisible() 30 | 31 | return true 32 | } 33 | 34 | func applicationWillResignActive(_ application: UIApplication) { 35 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 36 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 37 | } 38 | 39 | func applicationDidEnterBackground(_ application: UIApplication) { 40 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 41 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 42 | } 43 | 44 | func applicationWillEnterForeground(_ application: UIApplication) { 45 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 46 | } 47 | 48 | func applicationDidBecomeActive(_ application: UIApplication) { 49 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 50 | } 51 | 52 | func applicationWillTerminate(_ application: UIApplication) { 53 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 54 | } 55 | 56 | // MARK: - Private 57 | 58 | private static func makeNavigationContoller(rootViewController: UIViewController, title: String, image: UIImage?) 59 | -> UINavigationController 60 | { 61 | let controller = UINavigationController(rootViewController: rootViewController) 62 | controller.tabBarItem = UITabBarItem(title: title, image: image, selectedImage: nil) 63 | controller.navigationBar.barTintColor = .white 64 | return controller 65 | } 66 | 67 | private static func makeTabBarController() -> UITabBarController { 68 | let controller = UITabBarController() 69 | controller.tabBar.tintColor = .applicationTintColor 70 | controller.tabBar.barTintColor = .white 71 | return controller 72 | } 73 | 74 | } 75 | 76 | -------------------------------------------------------------------------------- /CustomPaging/Sources/CGPoint+Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGPoint+Utils.swift 3 | // CustomPaging 4 | // 5 | // Created by Ilya Lobanov on 26/08/2018. 6 | // Copyright © 2018 Ilya Lobanov. All rights reserved. 7 | // 8 | 9 | import CoreGraphics 10 | 11 | 12 | extension CGPoint { 13 | 14 | func distance(to point: CGPoint) -> CGFloat { 15 | return sqrt(pow((point.x - x), 2) + pow((point.y - y), 2)) 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /CustomPaging/Sources/Clamp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Clamp.swift 3 | // CustomPaging 4 | // 5 | // Created by Ilya Lobanov on 27/08/2018. 6 | // Copyright © 2018 Ilya Lobanov. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Comparable { 12 | 13 | func clamped(to limits: ClosedRange) -> Self { 14 | return min(max(self, limits.lowerBound), limits.upperBound) 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /CustomPaging/Sources/Collection/CollectionCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionCell.swift 3 | // CustomPaging 4 | // 5 | // Created by Ilya Lobanov on 26/08/2018. 6 | // Copyright © 2018 Ilya Lobanov. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class CollectionCell: UICollectionViewCell { 12 | 13 | struct Info { 14 | var text: String 15 | var textColor: UIColor 16 | var bgColor: UIColor 17 | var size: CGSize 18 | } 19 | 20 | override init(frame: CGRect) { 21 | super.init(frame: frame) 22 | 23 | contentView.layer.masksToBounds = true 24 | 25 | contentView.addSubview(textLabel) 26 | textLabel.font = .systemFont(ofSize: 16, weight: .bold) 27 | } 28 | 29 | required init?(coder aDecoder: NSCoder) { 30 | fatalError("init(coder:) has not been implemented") 31 | } 32 | 33 | func update(with info: Info) { 34 | textLabel.textAlignment = .center 35 | textLabel.text = info.text 36 | textLabel.textColor = info.textColor 37 | contentView.backgroundColor = info.bgColor 38 | } 39 | 40 | // MARK: - UIView 41 | 42 | override func layoutSubviews() { 43 | super.layoutSubviews() 44 | textLabel.frame = contentView.bounds 45 | contentView.layer.cornerRadius = contentView.bounds.height / 2 46 | } 47 | 48 | // MARK: - Private 49 | 50 | private let textLabel = UILabel() 51 | 52 | } 53 | -------------------------------------------------------------------------------- /CustomPaging/Sources/Collection/CollectionSettingsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionSettingsView.swift 3 | // CustomPaging 4 | // 5 | // Created by Ilya Lobanov on 27/08/2018. 6 | // Copyright © 2018 Ilya Lobanov. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol CollectionSettingsViewDelegate: class { 12 | func didChangeDeceleration(_ value: CGFloat) 13 | func didChangeSpringBounciness(_ value: CGFloat) 14 | func didChangeSpringSpeed(_ value: CGFloat) 15 | } 16 | 17 | final class CollectionSettingsView: UIView { 18 | 19 | struct Default { 20 | static let decelerationRate: CGFloat = UIScrollView.DecelerationRate.normal.rawValue 21 | static let decelerationRateLimits = UIScrollView.DecelerationRate.fast.rawValue...UIScrollView.DecelerationRate.normal.rawValue 22 | 23 | static let springBounciness: CGFloat = 4 24 | static let springBouncinessLimits: ClosedRange = 0.1...20 25 | 26 | static let springSpeed: CGFloat = 12 27 | static let springSpeedLimits: ClosedRange = 0.1...20 28 | } 29 | 30 | weak var delegate: CollectionSettingsViewDelegate? = nil 31 | 32 | var decelerationRate: CGFloat { 33 | return decelerationSlider.value 34 | } 35 | 36 | var springBounciness: CGFloat { 37 | return springBouncinessSlider.value 38 | } 39 | 40 | var springSpeed: CGFloat { 41 | return springSpeedSlider.value 42 | } 43 | 44 | override init(frame: CGRect) { 45 | super.init(frame: frame) 46 | 47 | setupViews() 48 | } 49 | 50 | required init?(coder aDecoder: NSCoder) { 51 | fatalError("init(coder:) has not been implemented") 52 | } 53 | 54 | // MARK: - Private 55 | 56 | private let decelerationSlider = SliderView() 57 | private let springBouncinessSlider = SliderView() 58 | private let springSpeedSlider = SliderView() 59 | private let stackView = UIStackView() 60 | 61 | private func setupViews() { 62 | addSubview(stackView) 63 | stackView.alignment = .fill 64 | stackView.axis = .vertical 65 | 66 | stackView.addArrangedSubview(decelerationSlider) 67 | decelerationSlider.title = "Deceleration Rate" 68 | decelerationSlider.limits = Default.decelerationRateLimits 69 | decelerationSlider.value = Default.decelerationRate 70 | decelerationSlider.defaultValue = Default.decelerationRate 71 | decelerationSlider.minTitle = "Fast" 72 | decelerationSlider.maxTitle = "Normal" 73 | decelerationSlider.onChange = { [weak self] value in 74 | self?.delegate?.didChangeDeceleration(value) 75 | } 76 | 77 | stackView.addArrangedSubview(springBouncinessSlider) 78 | springBouncinessSlider.title = "Spring Bounciness" 79 | springBouncinessSlider.limits = Default.springBouncinessLimits 80 | springBouncinessSlider.value = Default.springBounciness 81 | springBouncinessSlider.defaultValue = Default.springBounciness 82 | springBouncinessSlider.onChange = { [weak self] value in 83 | self?.delegate?.didChangeSpringBounciness(value) 84 | } 85 | 86 | stackView.addArrangedSubview(springSpeedSlider) 87 | springSpeedSlider.title = "Spring Speed" 88 | springSpeedSlider.limits = Default.springSpeedLimits 89 | springSpeedSlider.value = Default.springSpeed 90 | springSpeedSlider.defaultValue = Default.springSpeed 91 | springSpeedSlider.onChange = { [weak self] value in 92 | self?.delegate?.didChangeSpringSpeed(value) 93 | } 94 | 95 | setupLayout() 96 | } 97 | 98 | private func setupLayout() { 99 | let inset: CGFloat = 16 100 | 101 | stackView.spacing = 32 102 | stackView.translatesAutoresizingMaskIntoConstraints = false 103 | stackView.leftAnchor.constraint(equalTo: leftAnchor, constant: inset).isActive = true 104 | stackView.rightAnchor.constraint(equalTo: rightAnchor, constant: -inset).isActive = true 105 | stackView.topAnchor.constraint(equalTo: topAnchor, constant: inset).isActive = true 106 | stackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -inset).isActive = true 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /CustomPaging/Sources/Collection/CollectionViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionViewController.swift 3 | // CustomPaging 4 | // 5 | // Created by Ilya Lobanov on 25/08/2018. 6 | // Copyright © 2018 Ilya Lobanov. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class CollectionViewController: UIViewController { 12 | 13 | override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { 14 | collectionView = UICollectionView(frame: .zero, collectionViewLayout: Static.makeLayout()) 15 | pagingView = PagingView(contentView: collectionView) 16 | super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 17 | } 18 | 19 | required init?(coder aDecoder: NSCoder) { 20 | fatalError("init(coder:) has not been implemented") 21 | } 22 | 23 | // MARK: - UIViewController 24 | 25 | override func viewDidLoad() { 26 | super.viewDidLoad() 27 | 28 | title = "Collection" 29 | view.backgroundColor = .white 30 | 31 | collectionView.register(CollectionCell.self, forCellWithReuseIdentifier: Static.cellReuseIdentifier) 32 | collectionView.delegate = self 33 | collectionView.dataSource = self 34 | collectionView.backgroundColor = .white 35 | collectionView.showsHorizontalScrollIndicator = false 36 | 37 | contentScrollView.addSubview(pagingView) 38 | pagingView.anchors = anchors 39 | pagingView.decelerationRate = settingsView.decelerationRate 40 | pagingView.springBounciness = settingsView.springBounciness 41 | pagingView.springSpeed = settingsView.springSpeed 42 | 43 | contentScrollView.addSubview(settingsView) 44 | settingsView.delegate = self 45 | 46 | view.addSubview(contentScrollView) 47 | contentScrollView.alwaysBounceVertical = true 48 | contentScrollView.showsVerticalScrollIndicator = false 49 | 50 | setupLayout() 51 | } 52 | 53 | // MARK: - Private 54 | 55 | private let collectionView: UICollectionView 56 | private let pagingView: PagingView 57 | private let settingsView = CollectionSettingsView() 58 | private let contentScrollView = UIScrollView() 59 | private let cellInfos: [CollectionCell.Info] = Static.makeCellInfos() 60 | 61 | private var anchors: [CGPoint] { 62 | return (0.. [CollectionCell.Info] { 112 | return (cellColors + cellColors + cellColors).map { 113 | let text = String(format: "%06X", $0) 114 | let size = CGSize(width: round(.random(in: minCellWidth...maxCellWidth)), height: cellHeight) 115 | return CollectionCell.Info(text: text, textColor: .white, bgColor: UIColor(rgb: $0), size: size) 116 | } 117 | } 118 | 119 | static func makeLayout() -> UICollectionViewFlowLayout { 120 | let layout = UICollectionViewFlowLayout() 121 | layout.minimumInteritemSpacing = cellSpacing 122 | layout.sectionInset = UIEdgeInsets(top: cellSpacing, left: cellSpacing, bottom: cellSpacing, right: cellSpacing) 123 | layout.scrollDirection = .horizontal 124 | return layout 125 | } 126 | 127 | } 128 | 129 | } 130 | 131 | 132 | extension CollectionViewController: CollectionSettingsViewDelegate { 133 | 134 | func didChangeDeceleration(_ value: CGFloat) { 135 | pagingView.decelerationRate = value 136 | } 137 | 138 | func didChangeSpringBounciness(_ value: CGFloat) { 139 | pagingView.springBounciness = value 140 | } 141 | 142 | func didChangeSpringSpeed(_ value: CGFloat) { 143 | pagingView.springSpeed = value 144 | } 145 | 146 | } 147 | 148 | 149 | extension CollectionViewController: UICollectionViewDelegateFlowLayout { 150 | 151 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, 152 | sizeForItemAt indexPath: IndexPath) -> CGSize 153 | { 154 | return cellInfos[indexPath.item].size 155 | } 156 | 157 | func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, 158 | targetContentOffset: UnsafeMutablePointer) 159 | { 160 | pagingView.contentViewWillEndDragging(scrollView, withVelocity: velocity, targetContentOffset: targetContentOffset) 161 | } 162 | 163 | func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { 164 | pagingView.contentViewWillBeginDragging(scrollView) 165 | } 166 | 167 | } 168 | 169 | 170 | extension CollectionViewController: UICollectionViewDataSource { 171 | 172 | func numberOfSections(in collectionView: UICollectionView) -> Int { 173 | return 1 174 | } 175 | 176 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 177 | return cellInfos.count 178 | } 179 | 180 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) 181 | -> UICollectionViewCell 182 | { 183 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Static.cellReuseIdentifier, for: indexPath) 184 | (cell as? CollectionCell)?.update(with: cellInfos[indexPath.item]) 185 | return cell 186 | } 187 | 188 | } 189 | -------------------------------------------------------------------------------- /CustomPaging/Sources/Collection/SliderView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SliderView.swift 3 | // CustomPaging 4 | // 5 | // Created by Ilya Lobanov on 27/08/2018. 6 | // Copyright © 2018 Ilya Lobanov. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class SliderView: UIView { 12 | 13 | var value: CGFloat { 14 | get { 15 | return CGFloat(slider.value) 16 | } 17 | set { 18 | slider.value = Float(newValue) 19 | updateValueLabel() 20 | } 21 | } 22 | 23 | var defaultValue: CGFloat? = nil 24 | 25 | var limits: ClosedRange { 26 | get { 27 | return CGFloat(slider.minimumValue)...CGFloat(slider.maximumValue) 28 | } 29 | set { 30 | slider.minimumValue = Float(newValue.lowerBound) 31 | slider.maximumValue = Float(newValue.upperBound) 32 | updateLimitTitles() 33 | } 34 | } 35 | 36 | var title: String = "" { 37 | didSet { 38 | titleButton.setTitle(title, for: .normal) 39 | } 40 | } 41 | 42 | var minTitle: String? { 43 | didSet { 44 | updateLimitTitles() 45 | } 46 | } 47 | 48 | var maxTitle: String? { 49 | didSet { 50 | updateLimitTitles() 51 | } 52 | } 53 | 54 | var onChange: ((CGFloat) -> Void)? = nil 55 | 56 | override init(frame: CGRect) { 57 | super.init(frame: frame) 58 | setupViews() 59 | } 60 | 61 | required init?(coder aDecoder: NSCoder) { 62 | fatalError("init(coder:) has not been implemented") 63 | } 64 | 65 | // MARK: - Private 66 | 67 | private let slider = UISlider() 68 | private let titleButton = UIButton(type: .system) 69 | private let minLabel = UILabel() 70 | private let maxLabel = UILabel() 71 | private let valueLabel = UILabel() 72 | 73 | private func setupViews() { 74 | addSubview(slider) 75 | slider.addTarget(self, action: #selector(SliderView.handleSlider), for: .valueChanged) 76 | slider.thumbTintColor = .applicationTintColor 77 | slider.minimumTrackTintColor = .lightGray 78 | slider.maximumTrackTintColor = .lightGray 79 | 80 | addSubview(titleButton) 81 | titleButton.titleLabel?.font = .systemFont(ofSize: 16, weight: .bold) 82 | titleButton.contentHorizontalAlignment = .left 83 | titleButton.tintColor = .black 84 | titleButton.addTarget(self, action: #selector(handleTitleButton), for: .touchUpInside) 85 | 86 | addSubview(minLabel) 87 | minLabel.font = .systemFont(ofSize: 14, weight: .regular) 88 | 89 | addSubview(maxLabel) 90 | maxLabel.textAlignment = .right 91 | maxLabel.font = .systemFont(ofSize: 14, weight: .regular) 92 | 93 | addSubview(valueLabel) 94 | valueLabel.textAlignment = .center 95 | valueLabel.font = .systemFont(ofSize: 14, weight: .bold) 96 | 97 | setupLayout() 98 | updateLimitTitles() 99 | } 100 | 101 | private func setupLayout() { 102 | titleButton.translatesAutoresizingMaskIntoConstraints = false 103 | titleButton.leftAnchor.constraint(equalTo: leftAnchor).isActive = true 104 | titleButton.topAnchor.constraint(equalTo: topAnchor).isActive = true 105 | titleButton.rightAnchor.constraint(equalTo: rightAnchor).isActive = true 106 | 107 | slider.translatesAutoresizingMaskIntoConstraints = false 108 | slider.leftAnchor.constraint(equalTo: leftAnchor).isActive = true 109 | slider.rightAnchor.constraint(equalTo: rightAnchor).isActive = true 110 | 111 | minLabel.translatesAutoresizingMaskIntoConstraints = false 112 | minLabel.leftAnchor.constraint(equalTo: leftAnchor).isActive = true 113 | minLabel.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true 114 | 115 | maxLabel.translatesAutoresizingMaskIntoConstraints = false 116 | maxLabel.rightAnchor.constraint(equalTo: rightAnchor).isActive = true 117 | maxLabel.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true 118 | 119 | valueLabel.translatesAutoresizingMaskIntoConstraints = false 120 | valueLabel.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true 121 | valueLabel.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true 122 | 123 | minLabel.rightAnchor.constraint(equalTo: valueLabel.leftAnchor).isActive = true 124 | valueLabel.rightAnchor.constraint(equalTo: maxLabel.leftAnchor).isActive = true 125 | titleButton.bottomAnchor.constraint(equalTo: slider.topAnchor).isActive = true 126 | slider.bottomAnchor.constraint(equalTo: maxLabel.topAnchor).isActive = true 127 | slider.bottomAnchor.constraint(equalTo: minLabel.topAnchor).isActive = true 128 | } 129 | 130 | private func updateValueLabel() { 131 | valueLabel.text = value.stringDescription 132 | } 133 | 134 | private func updateLimitTitles() { 135 | minLabel.text = minTitle ?? limits.lowerBound.stringDescription 136 | maxLabel.text = maxTitle ?? limits.upperBound.stringDescription 137 | } 138 | 139 | @objc private func handleSlider() { 140 | updateValueLabel() 141 | onChange?(value) 142 | } 143 | 144 | @objc private func handleTitleButton() { 145 | if let v = defaultValue { 146 | value = v 147 | } 148 | } 149 | 150 | } 151 | 152 | private extension CGFloat { 153 | 154 | var stringDescription: String { 155 | let formatter = NumberFormatter() 156 | formatter.minimumIntegerDigits = 1 157 | formatter.minimumFractionDigits = 0 158 | formatter.maximumFractionDigits = 4 159 | return formatter.string(from: NSNumber(value: Double(self))) ?? "\(self)" 160 | } 161 | 162 | } 163 | -------------------------------------------------------------------------------- /CustomPaging/Sources/PagingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PagingView.swift 3 | // CustomPaging 4 | // 5 | // Created by Ilya Lobanov on 26/08/2018. 6 | // Copyright © 2018 Ilya Lobanov. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import pop 11 | 12 | 13 | final class PagingView: UIView { 14 | 15 | let contentView: UIScrollView 16 | 17 | var anchors: [CGPoint] = [] 18 | 19 | var decelerationRate: CGFloat = UIScrollView.DecelerationRate.fast.rawValue 20 | 21 | /** 22 | @abstract The effective bounciness. 23 | @discussion Use in conjunction with 'springSpeed' to change animation effect. Values are converted into corresponding dynamics constants. Higher values increase spring movement range resulting in more oscillations and springiness. Defined as a value in the range [0, 20]. Defaults to 4. 24 | */ 25 | var springBounciness: CGFloat = 4 26 | 27 | /** 28 | @abstract The effective speed. 29 | @discussion Use in conjunction with 'springBounciness' to change animation effect. Values are converted into corresponding dynamics constants. Higher values increase the dampening power of the spring resulting in a faster initial velocity and more rapid bounce slowdown. Defined as a value in the range [0, 20]. Defaults to 12. 30 | */ 31 | var springSpeed: CGFloat = 12 32 | 33 | init(contentView: UIScrollView) { 34 | self.contentView = contentView 35 | 36 | super.init(frame: .zero) 37 | 38 | setupViews() 39 | } 40 | 41 | required init?(coder aDecoder: NSCoder) { 42 | fatalError("init(coder:) has not been implemented") 43 | } 44 | 45 | func contentViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, 46 | targetContentOffset: UnsafeMutablePointer) 47 | { 48 | // Stop system animation 49 | targetContentOffset.pointee = scrollView.contentOffset 50 | 51 | let offsetProjection = scrollView.contentOffset.project(initialVelocity: velocity, 52 | decelerationRate: decelerationRate) 53 | 54 | if let target = nearestAnchor(forContentOffset: offsetProjection) { 55 | snapAnimated(toContentOffset: target, velocity: velocity) 56 | } 57 | } 58 | 59 | func contentViewWillBeginDragging(_ scrollView: UIScrollView) { 60 | stopSnappingAnimation() 61 | } 62 | 63 | // MARK: - Private 64 | 65 | private var minAnchor: CGPoint { 66 | let x = -contentView.adjustedContentInset.left 67 | let y = -contentView.adjustedContentInset.top 68 | return CGPoint(x: x, y: y) 69 | } 70 | 71 | private var maxAnchor: CGPoint { 72 | let x = contentView.contentSize.width - bounds.width + contentView.adjustedContentInset.right 73 | let y = contentView.contentSize.height - bounds.height + contentView.adjustedContentInset.bottom 74 | return CGPoint(x: x, y: y) 75 | } 76 | 77 | private func setupViews() { 78 | addSubview(contentView) 79 | setupLayout() 80 | } 81 | 82 | private func setupLayout() { 83 | contentView.translatesAutoresizingMaskIntoConstraints = false 84 | contentView.topAnchor.constraint(equalTo: topAnchor).isActive = true 85 | contentView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true 86 | contentView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true 87 | contentView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true 88 | } 89 | 90 | private func nearestAnchor(forContentOffset offset: CGPoint) -> CGPoint? { 91 | guard let candidate = anchors.min(by: { offset.distance(to: $0) < offset.distance(to: $1) }) else { 92 | return nil 93 | } 94 | 95 | let x = candidate.x.clamped(to: minAnchor.x...maxAnchor.x) 96 | let y = candidate.y.clamped(to: minAnchor.y...maxAnchor.y) 97 | 98 | return CGPoint(x: x, y: y) 99 | } 100 | 101 | // MARK: - Private: Animation 102 | 103 | private static let snappingAnimationKey = "CustomPaging.PagingView.scrollView.snappingAnimation" 104 | 105 | private func snapAnimated(toContentOffset newOffset: CGPoint, velocity: CGPoint) { 106 | let animation: POPSpringAnimation = POPSpringAnimation(propertyNamed: kPOPScrollViewContentOffset) 107 | animation.velocity = velocity 108 | animation.toValue = newOffset 109 | animation.fromValue = contentView.contentOffset 110 | animation.springBounciness = springBounciness 111 | animation.springSpeed = springSpeed 112 | 113 | contentView.pop_add(animation, forKey: PagingView.snappingAnimationKey) 114 | } 115 | 116 | private func stopSnappingAnimation() { 117 | contentView.pop_removeAnimation(forKey: PagingView.snappingAnimationKey) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /CustomPaging/Sources/Projection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Projection.swift 3 | // CustomPaging 4 | // 5 | // Created by Ilya Lobanov on 25/08/2018. 6 | // Copyright © 2018 Ilya Lobanov. All rights reserved. 7 | // 8 | 9 | import CoreGraphics 10 | 11 | extension FloatingPoint { 12 | 13 | func project(initialVelocity: Self, decelerationRate: Self) -> Self { 14 | if decelerationRate >= 1 { 15 | assert(false) 16 | return self 17 | } 18 | 19 | return self + initialVelocity * decelerationRate / (1 - decelerationRate) 20 | } 21 | 22 | } 23 | 24 | extension CGPoint { 25 | 26 | func project(initialVelocity: CGPoint, decelerationRate: CGPoint) -> CGPoint { 27 | let xProjection = x.project(initialVelocity: initialVelocity.x, decelerationRate: decelerationRate.x) 28 | let yProjection = y.project(initialVelocity: initialVelocity.y, decelerationRate: decelerationRate.y) 29 | return CGPoint(x: xProjection, y: yProjection) 30 | } 31 | 32 | func project(initialVelocity: CGPoint, decelerationRate: CGFloat) -> CGPoint { 33 | return project(initialVelocity: initialVelocity, decelerationRate: CGPoint(x: decelerationRate, y: decelerationRate)) 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /CustomPaging/Sources/Table/CellInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CellInfo.swift 3 | // CustomPaging 4 | // 5 | // Created by Ilya Lobanov on 04/08/2018. 6 | // Copyright © 2018 Ilya Lobanov. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | struct TableCellInfo { 12 | var text: String 13 | var textColor: UIColor 14 | var bgColor: UIColor 15 | var height: CGFloat 16 | var font: UIFont 17 | } 18 | 19 | extension UITableViewCell { 20 | 21 | func update(with info: TableCellInfo) { 22 | textLabel?.textAlignment = .center 23 | textLabel?.text = info.text 24 | textLabel?.textColor = info.textColor 25 | textLabel?.font = info.font 26 | backgroundColor = info.bgColor 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /CustomPaging/Sources/Table/TableCellInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableCellInfo.swift 3 | // CustomPaging 4 | // 5 | // Created by Ilya Lobanov on 04/08/2018. 6 | // Copyright © 2018 Ilya Lobanov. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | struct TableCellInfo { 12 | var text: String 13 | var textColor: UIColor 14 | var bgColor: UIColor 15 | var height: CGFloat 16 | var font: UIFont 17 | } 18 | 19 | extension UITableViewCell { 20 | 21 | func update(with info: TableCellInfo) { 22 | textLabel?.textAlignment = .center 23 | textLabel?.text = info.text 24 | textLabel?.textColor = info.textColor 25 | textLabel?.font = info.font 26 | backgroundColor = info.bgColor 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /CustomPaging/Sources/Table/TableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewController.swift 3 | // CustomPaging 4 | // 5 | // Created by Ilya Lobanov on 04/08/2018. 6 | // Copyright © 2018 Ilya Lobanov. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import pop 11 | 12 | final class TableViewController: UIViewController { 13 | 14 | // MARK: - UIViewController 15 | 16 | override func viewDidLoad() { 17 | super.viewDidLoad() 18 | 19 | title = "Table" 20 | 21 | view.addSubview(tableView) 22 | tableView.separatorStyle = .none 23 | tableView.allowsSelection = false 24 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: Static.cellReuseIdentifier) 25 | tableView.delegate = self 26 | tableView.dataSource = self 27 | 28 | tableView.frame = view.bounds 29 | tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight] 30 | } 31 | 32 | // MARK: - Private 33 | 34 | private let tableView = UITableView() 35 | 36 | private let cellInfos: [TableCellInfo] = Static.makeCellInfos() 37 | 38 | private var anchors: [CGPoint] { 39 | return (0.. CGPoint { 52 | var candidate = anchors.min(by: { abs($0.y - offset.y) < abs($1.y - offset.y) })! 53 | candidate.y = min(candidate.y, maxAnchor.y) 54 | return candidate 55 | } 56 | 57 | // MARK: - Private: Static 58 | 59 | private struct Static { 60 | 61 | static let minCellHeight: CGFloat = 128 62 | 63 | static let maxCellHeight: CGFloat = 384 64 | 65 | static let cellReuseIdentifier = "\(UITableViewCell.self)" 66 | 67 | static let cellColors: [UInt] = [0xB11F38, 0xE77A39, 0xEBD524, 0x4AA77A, 0x685B87, 0xA24C57] 68 | 69 | static func makeCellInfos() -> [TableCellInfo] { 70 | return (cellColors + cellColors + cellColors).map { 71 | TableCellInfo(text: String(format: "%06X", $0), 72 | textColor: .white, 73 | bgColor: UIColor(rgb: $0), 74 | height: round(.random(in: minCellHeight...maxCellHeight)), 75 | font: .systemFont(ofSize: 32, weight: .medium)) 76 | } 77 | } 78 | 79 | } 80 | 81 | } 82 | 83 | extension TableViewController: UITableViewDataSource { 84 | 85 | func numberOfSections(in tableView: UITableView) -> Int { 86 | return 1 87 | } 88 | 89 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 90 | return cellInfos.count 91 | } 92 | 93 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 94 | let cell = tableView.dequeueReusableCell(withIdentifier: Static.cellReuseIdentifier, for: indexPath) 95 | 96 | cell.update(with: cellInfos[indexPath.row]) 97 | 98 | return cell 99 | } 100 | 101 | } 102 | 103 | extension TableViewController: UITableViewDelegate { 104 | 105 | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 106 | return cellInfos[indexPath.row].height 107 | } 108 | 109 | func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, 110 | targetContentOffset: UnsafeMutablePointer) 111 | { 112 | let decelerationRate = (UIScrollView.DecelerationRate.normal.rawValue + UIScrollView.DecelerationRate.fast.rawValue) / 2 113 | let offsetProjection = scrollView.contentOffset.project(initialVelocity: velocity, decelerationRate: decelerationRate) 114 | let targetAnchor = nearestAnchor(forContentOffset: offsetProjection) 115 | 116 | // Stop system animation 117 | targetContentOffset.pointee = scrollView.contentOffset 118 | 119 | scrollView.snapAnimated(toContentOffset: targetAnchor, velocity: velocity) 120 | } 121 | 122 | func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { 123 | scrollView.stopSnappingAnimation() 124 | } 125 | } 126 | 127 | private extension UIScrollView { 128 | 129 | private static let snappingAnimationKey = "CustomPaging.TableViewController.scrollView.snappingAnimation" 130 | 131 | func snapAnimated(toContentOffset newOffset: CGPoint, velocity: CGPoint) { 132 | let animation: POPSpringAnimation = POPSpringAnimation(propertyNamed: kPOPScrollViewContentOffset) 133 | animation.velocity = velocity 134 | animation.toValue = newOffset 135 | animation.fromValue = contentOffset 136 | 137 | pop_add(animation, forKey: UIScrollView.snappingAnimationKey) 138 | } 139 | 140 | func stopSnappingAnimation() { 141 | pop_removeAnimation(forKey: UIScrollView.snappingAnimationKey) 142 | } 143 | 144 | } 145 | -------------------------------------------------------------------------------- /CustomPaging/Sources/UIColor+Application.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+Application.swift 3 | // CustomPaging 4 | // 5 | // Created by Ilya Lobanov on 27/08/2018. 6 | // Copyright © 2018 Ilya Lobanov. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIColor { 12 | 13 | static var applicationTintColor: UIColor { 14 | return UIColor(rgb: 0xB11F38) 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /CustomPaging/Sources/UIColor+Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+Utils.swift 3 | // CustomPaging 4 | // 5 | // Created by Ilya Lobanov on 04/08/2018. 6 | // Copyright © 2018 Ilya Lobanov. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIColor { 12 | 13 | convenience init(rgb value: UInt) { 14 | self.init(byteRed: UInt8((value >> 16) & 0xff), 15 | green: UInt8((value >> 8) & 0xff), 16 | blue: UInt8(value & 0xff), 17 | alpha: 0xff) 18 | } 19 | 20 | convenience init(byteRed red: UInt8, green: UInt8, blue: UInt8, alpha: UInt8 = 0xff) { 21 | self.init(red: CGFloat(red) / 255, 22 | green: CGFloat(green) / 255, 23 | blue: CGFloat(blue) / 255, 24 | alpha: CGFloat(alpha) / 255) 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | target 'CustomPaging' do 2 | use_frameworks! 3 | pod 'pop' 4 | end 5 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - pop (1.0.10) 3 | 4 | DEPENDENCIES: 5 | - pop 6 | 7 | SPEC REPOS: 8 | https://github.com/cocoapods/specs.git: 9 | - pop 10 | 11 | SPEC CHECKSUMS: 12 | pop: 82ca6b068ce9278fd350fd9dd09482a0ce9492e6 13 | 14 | PODFILE CHECKSUM: 4b3492768a5415b16d9981ca899507c11915352a 15 | 16 | COCOAPODS: 1.5.3 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Custom Paging 2 | This is a demo project for [the article](https://medium.com/yandex-maps-ios/custom-paging-в-ios-c4dd4611e589). 3 | --------------------------------------------------------------------------------