├── .gitignore ├── APLoopingScrollView.podspec ├── APLoopingScrollView ├── APLoopingScrollView.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ └── contents.xcworkspacedata ├── APLoopingScrollView │ ├── Examples │ │ ├── AppDelegate.swift │ │ ├── Examples.swift │ │ ├── LoopingScrollViewWithScaling.swift │ │ └── ViewController.swift │ ├── Images.xcassets │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Info.plist │ ├── Source │ │ └── APLoopingScrollView.swift │ └── gifs │ │ ├── horz.gif │ │ └── vert.gif ├── APLoopingScrollViewTests │ ├── APLoopingScrollViewTests.swift │ └── Info.plist ├── Images.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-40@2x.png │ │ ├── Icon-60@2x.png │ │ └── Icon-60@3x.png │ └── iTunesArtwork.imageset │ │ ├── Contents.json │ │ ├── iTunesArtwork.png │ │ └── iTunesArtwork@2x.png ├── LaunchScreen.xib └── Storyboard.storyboard ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | build/ 4 | *.pbxuser 5 | !default.pbxuser 6 | *.mode1v3 7 | !default.mode1v3 8 | *.mode2v3 9 | !default.mode2v3 10 | *.perspectivev3 11 | !default.perspectivev3 12 | xcuserdata 13 | *.xccheckout 14 | *.moved-aside 15 | DerivedData 16 | *.hmap 17 | *.ipa 18 | *.xcuserstate 19 | *.DS_Store 20 | .DS_Store 21 | 22 | # CocoaPods 23 | # 24 | # We recommend against adding the Pods directory to your .gitignore. However 25 | # you should judge for yourself, the pros and cons are mentioned at: 26 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 27 | # 28 | # Pods/ 29 | 30 | # Carthage 31 | # 32 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 33 | # Carthage/Checkouts 34 | 35 | Carthage/Build 36 | -------------------------------------------------------------------------------- /APLoopingScrollView.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod spec lint APLoopingScrollView.podspec' to ensure this is a 3 | # valid spec and to remove all comments including this before submitting the spec. 4 | # 5 | # To learn more about Podspec attributes see http://docs.cocoapods.org/specification.html 6 | # To see working Podspecs in the CocoaPods repo see https://github.com/CocoaPods/Specs/ 7 | # 8 | 9 | Pod::Spec.new do |s| 10 | 11 | # ――― Spec Metadata ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 12 | # 13 | # These will help people to find your library, and whilst it 14 | # can feel like a chore to fill in it's definitely to your advantage. The 15 | # summary should be tweet-length, and the description more in depth. 16 | # 17 | 18 | s.name = "APLoopingScrollView" 19 | s.version = "1.0.2" 20 | s.summary = "Looping UIScrollView implementation, supports custom page size and item spacing. Built in Swift" 21 | 22 | s.description = "After failing to find a decent looping scroll view impelementation I set out to build my own. APLoopingScrollView is a direct subclass of UIScrollView that displays collections of cards in either horizontal or vertical orientation. 23 | 24 | You have control over: 25 | 26 | * Item Size 27 | * Item Spacing 28 | * Scroll Direction 29 | * Paging" 30 | 31 | s.homepage = "https://github.com/MrBendel/APLoopingScrollView" 32 | s.screenshots = "https://raw.githubusercontent.com/MrBendel/APLoopingScrollView/master/APLoopingScrollView/APLoopingScrollView/gifs/horz.gif", "https://raw.githubusercontent.com/MrBendel/APLoopingScrollView/master/APLoopingScrollView/APLoopingScrollView/gifs/vert.gif" 33 | 34 | 35 | # ――― Spec License ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 36 | # 37 | # Licensing your code is important. See http://choosealicense.com for more info. 38 | # CocoaPods will detect a license file if there is a named LICENSE* 39 | # Popular ones are 'MIT', 'BSD' and 'Apache License, Version 2.0'. 40 | # 41 | 42 | s.license = "MIT" 43 | s.license = { :type => "MIT", :file => "LICENSE" } 44 | 45 | 46 | # ――― Author Metadata ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 47 | # 48 | # Specify the authors of the library, with email addresses. Email addresses 49 | # of the authors are extracted from the SCM log. E.g. $ git log. CocoaPods also 50 | # accepts just a name if you'd rather not provide an email address. 51 | # 52 | # Specify a social_media_url where others can refer to, for example a twitter 53 | # profile URL. 54 | # 55 | 56 | s.author = { "Andrew Poes" => "doniguan@gmail.com" } 57 | s.social_media_url = "http://twitter.com/doniguan" 58 | 59 | # ――― Platform Specifics ――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 60 | # 61 | # If this Pod runs only on iOS or OS X, then specify the platform and 62 | # the deployment target. You can optionally include the target after the platform. 63 | # 64 | 65 | s.platform = :ios 66 | s.platform = :ios, "8.0" 67 | 68 | # When using multiple platforms 69 | # s.ios.deployment_target = "8.0" 70 | # s.osx.deployment_target = "10.7" 71 | # s.watchos.deployment_target = "2.0" 72 | 73 | 74 | # ――― Source Location ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 75 | # 76 | # Specify the location from where the source should be retrieved. 77 | # Supports git, hg, bzr, svn and HTTP. 78 | # 79 | 80 | s.source = { :git => "https://github.com/MrBendel/APLoopingScrollView.git", :tag => "1.0.2", :commit => "eb28db91771320665ab8e676ba28f8320c6a5957" } 81 | 82 | 83 | # ――― Source Code ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 84 | # 85 | # CocoaPods is smart about how it includes source code. For source files 86 | # giving a folder will include any swift, h, m, mm, c & cpp files. 87 | # For header files it will include any header in the folder. 88 | # Not including the public_header_files will make all headers public. 89 | # 90 | 91 | s.source_files = "APLoopingScrollView/APLoopingScrollView/Source/", "APLoopingScrollView/APLoopingScrollView/Source/**/*.{h,m}" 92 | s.exclude_files = "Classes/Examples" 93 | 94 | # s.public_header_files = "Classes/**/*.h" 95 | 96 | 97 | # ――― Resources ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 98 | # 99 | # A list of resources included with the Pod. These are copied into the 100 | # target bundle with a build phase script. Anything else will be cleaned. 101 | # You can preserve files from being cleaned, please don't preserve 102 | # non-essential files like tests, examples and documentation. 103 | # 104 | 105 | # s.resource = "icon.png" 106 | # s.resources = "Resources/*.png" 107 | 108 | # s.preserve_paths = "FilesToSave", "MoreFilesToSave" 109 | 110 | 111 | # ――― Project Linking ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 112 | # 113 | # Link your library with frameworks, or libraries. Libraries do not include 114 | # the lib prefix of their name. 115 | # 116 | 117 | # s.framework = "SomeFramework" 118 | # s.frameworks = "SomeFramework", "AnotherFramework" 119 | 120 | # s.library = "iconv" 121 | # s.libraries = "iconv", "xml2" 122 | 123 | 124 | # ――― Project Settings ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 125 | # 126 | # If your library depends on compiler flags you can set them in the xcconfig hash 127 | # where they will only apply to your library. If you depend on other Podspecs 128 | # you can include multiple dependencies to ensure it works. 129 | 130 | # s.requires_arc = true 131 | 132 | # s.xcconfig = { "HEADER_SEARCH_PATHS" => "$(SDKROOT)/usr/include/libxml2" } 133 | # s.dependency "JSONKit", "~> 1.4" 134 | 135 | end 136 | -------------------------------------------------------------------------------- /APLoopingScrollView/APLoopingScrollView.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | A75971EF1BA7286E0042D3C4 /* LoopingScrollViewWithScaling.swift in Sources */ = {isa = PBXBuildFile; fileRef = A75971EE1BA7286E0042D3C4 /* LoopingScrollViewWithScaling.swift */; }; 11 | A75971F11BA762A80042D3C4 /* Examples.swift in Sources */ = {isa = PBXBuildFile; fileRef = A75971F01BA762A80042D3C4 /* Examples.swift */; }; 12 | A75A58F51B8C2D810075BCF8 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A75A58F41B8C2D810075BCF8 /* AppDelegate.swift */; }; 13 | A75A58F71B8C2D810075BCF8 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A75A58F61B8C2D810075BCF8 /* ViewController.swift */; }; 14 | A75A59171B8C2ECC0075BCF8 /* APLoopingScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A75A59161B8C2ECC0075BCF8 /* APLoopingScrollView.swift */; }; 15 | A7A346A61BA707D300064306 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A7A346A51BA707D300064306 /* Images.xcassets */; }; 16 | A7A346D81BA7093900064306 /* Storyboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A7A346D71BA7093900064306 /* Storyboard.storyboard */; }; 17 | A7A346DA1BA7094500064306 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = A7A346D91BA7094500064306 /* LaunchScreen.xib */; }; 18 | FAA364811D0D39D900920559 /* APLoopingScrollViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAA364801D0D39D900920559 /* APLoopingScrollViewTests.swift */; }; 19 | /* End PBXBuildFile section */ 20 | 21 | /* Begin PBXContainerItemProxy section */ 22 | FAA364831D0D39D900920559 /* PBXContainerItemProxy */ = { 23 | isa = PBXContainerItemProxy; 24 | containerPortal = A75A58E71B8C2D810075BCF8 /* Project object */; 25 | proxyType = 1; 26 | remoteGlobalIDString = A75A58EE1B8C2D810075BCF8; 27 | remoteInfo = APLoopingScrollView; 28 | }; 29 | /* End PBXContainerItemProxy section */ 30 | 31 | /* Begin PBXFileReference section */ 32 | A75971EE1BA7286E0042D3C4 /* LoopingScrollViewWithScaling.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = LoopingScrollViewWithScaling.swift; path = APLoopingScrollView/Examples/LoopingScrollViewWithScaling.swift; sourceTree = SOURCE_ROOT; }; 33 | A75971F01BA762A80042D3C4 /* Examples.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Examples.swift; path = APLoopingScrollView/Examples/Examples.swift; sourceTree = SOURCE_ROOT; }; 34 | A75A58EF1B8C2D810075BCF8 /* APLoopingScrollView.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = APLoopingScrollView.app; sourceTree = BUILT_PRODUCTS_DIR; }; 35 | A75A58F31B8C2D810075BCF8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; name = Info.plist; path = APLoopingScrollView/Info.plist; sourceTree = ""; }; 36 | A75A58F41B8C2D810075BCF8 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = APLoopingScrollView/Examples/AppDelegate.swift; sourceTree = SOURCE_ROOT; }; 37 | A75A58F61B8C2D810075BCF8 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ViewController.swift; path = APLoopingScrollView/Examples/ViewController.swift; sourceTree = SOURCE_ROOT; }; 38 | A75A59161B8C2ECC0075BCF8 /* APLoopingScrollView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = APLoopingScrollView.swift; path = APLoopingScrollView/Source/APLoopingScrollView.swift; sourceTree = SOURCE_ROOT; }; 39 | A7A346A51BA707D300064306 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = SOURCE_ROOT; }; 40 | A7A346D71BA7093900064306 /* Storyboard.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Storyboard.storyboard; sourceTree = SOURCE_ROOT; }; 41 | A7A346D91BA7094500064306 /* LaunchScreen.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = LaunchScreen.xib; sourceTree = SOURCE_ROOT; }; 42 | FAA3647E1D0D39D900920559 /* APLoopingScrollViewTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = APLoopingScrollViewTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 43 | FAA364801D0D39D900920559 /* APLoopingScrollViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APLoopingScrollViewTests.swift; sourceTree = ""; }; 44 | FAA364821D0D39D900920559 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 45 | /* End PBXFileReference section */ 46 | 47 | /* Begin PBXFrameworksBuildPhase section */ 48 | A75A58EC1B8C2D810075BCF8 /* Frameworks */ = { 49 | isa = PBXFrameworksBuildPhase; 50 | buildActionMask = 2147483647; 51 | files = ( 52 | ); 53 | runOnlyForDeploymentPostprocessing = 0; 54 | }; 55 | FAA3647B1D0D39D900920559 /* Frameworks */ = { 56 | isa = PBXFrameworksBuildPhase; 57 | buildActionMask = 2147483647; 58 | files = ( 59 | ); 60 | runOnlyForDeploymentPostprocessing = 0; 61 | }; 62 | /* End PBXFrameworksBuildPhase section */ 63 | 64 | /* Begin PBXGroup section */ 65 | A75971ED1BA728500042D3C4 /* Scale Scroll */ = { 66 | isa = PBXGroup; 67 | children = ( 68 | A75971EE1BA7286E0042D3C4 /* LoopingScrollViewWithScaling.swift */, 69 | ); 70 | name = "Scale Scroll"; 71 | sourceTree = ""; 72 | }; 73 | A75A58E61B8C2D810075BCF8 = { 74 | isa = PBXGroup; 75 | children = ( 76 | A75A58F11B8C2D810075BCF8 /* APLoopingScrollView */, 77 | FAA3647F1D0D39D900920559 /* APLoopingScrollViewTests */, 78 | A75A58F01B8C2D810075BCF8 /* Products */, 79 | ); 80 | sourceTree = ""; 81 | }; 82 | A75A58F01B8C2D810075BCF8 /* Products */ = { 83 | isa = PBXGroup; 84 | children = ( 85 | A75A58EF1B8C2D810075BCF8 /* APLoopingScrollView.app */, 86 | FAA3647E1D0D39D900920559 /* APLoopingScrollViewTests.xctest */, 87 | ); 88 | name = Products; 89 | sourceTree = ""; 90 | }; 91 | A75A58F11B8C2D810075BCF8 /* APLoopingScrollView */ = { 92 | isa = PBXGroup; 93 | children = ( 94 | A75A59151B8C2EBE0075BCF8 /* Example */, 95 | A75A59141B8C2EB50075BCF8 /* InfiniteScrollView Source */, 96 | A7DFC2E11BA7029500CB87A9 /* Resources */, 97 | A75A58F21B8C2D810075BCF8 /* Supporting Files */, 98 | ); 99 | name = APLoopingScrollView; 100 | path = InfinityScrollView; 101 | sourceTree = ""; 102 | }; 103 | A75A58F21B8C2D810075BCF8 /* Supporting Files */ = { 104 | isa = PBXGroup; 105 | children = ( 106 | A75A58F31B8C2D810075BCF8 /* Info.plist */, 107 | ); 108 | name = "Supporting Files"; 109 | sourceTree = ""; 110 | }; 111 | A75A59141B8C2EB50075BCF8 /* InfiniteScrollView Source */ = { 112 | isa = PBXGroup; 113 | children = ( 114 | A75A59161B8C2ECC0075BCF8 /* APLoopingScrollView.swift */, 115 | ); 116 | name = "InfiniteScrollView Source"; 117 | sourceTree = ""; 118 | }; 119 | A75A59151B8C2EBE0075BCF8 /* Example */ = { 120 | isa = PBXGroup; 121 | children = ( 122 | A75971ED1BA728500042D3C4 /* Scale Scroll */, 123 | A75A58F41B8C2D810075BCF8 /* AppDelegate.swift */, 124 | A75A58F61B8C2D810075BCF8 /* ViewController.swift */, 125 | A75971F01BA762A80042D3C4 /* Examples.swift */, 126 | ); 127 | name = Example; 128 | sourceTree = ""; 129 | }; 130 | A7DFC2E11BA7029500CB87A9 /* Resources */ = { 131 | isa = PBXGroup; 132 | children = ( 133 | A7A346A51BA707D300064306 /* Images.xcassets */, 134 | A7A346D71BA7093900064306 /* Storyboard.storyboard */, 135 | A7A346D91BA7094500064306 /* LaunchScreen.xib */, 136 | ); 137 | name = Resources; 138 | sourceTree = ""; 139 | }; 140 | FAA3647F1D0D39D900920559 /* APLoopingScrollViewTests */ = { 141 | isa = PBXGroup; 142 | children = ( 143 | FAA364801D0D39D900920559 /* APLoopingScrollViewTests.swift */, 144 | FAA364821D0D39D900920559 /* Info.plist */, 145 | ); 146 | path = APLoopingScrollViewTests; 147 | sourceTree = ""; 148 | }; 149 | /* End PBXGroup section */ 150 | 151 | /* Begin PBXNativeTarget section */ 152 | A75A58EE1B8C2D810075BCF8 /* APLoopingScrollView */ = { 153 | isa = PBXNativeTarget; 154 | buildConfigurationList = A75A590E1B8C2D820075BCF8 /* Build configuration list for PBXNativeTarget "APLoopingScrollView" */; 155 | buildPhases = ( 156 | A75A58EB1B8C2D810075BCF8 /* Sources */, 157 | A75A58EC1B8C2D810075BCF8 /* Frameworks */, 158 | A75A58ED1B8C2D810075BCF8 /* Resources */, 159 | ); 160 | buildRules = ( 161 | ); 162 | dependencies = ( 163 | ); 164 | name = APLoopingScrollView; 165 | productName = InfinityScrollView; 166 | productReference = A75A58EF1B8C2D810075BCF8 /* APLoopingScrollView.app */; 167 | productType = "com.apple.product-type.application"; 168 | }; 169 | FAA3647D1D0D39D900920559 /* APLoopingScrollViewTests */ = { 170 | isa = PBXNativeTarget; 171 | buildConfigurationList = FAA364851D0D39D900920559 /* Build configuration list for PBXNativeTarget "APLoopingScrollViewTests" */; 172 | buildPhases = ( 173 | FAA3647A1D0D39D900920559 /* Sources */, 174 | FAA3647B1D0D39D900920559 /* Frameworks */, 175 | FAA3647C1D0D39D900920559 /* Resources */, 176 | ); 177 | buildRules = ( 178 | ); 179 | dependencies = ( 180 | FAA364841D0D39D900920559 /* PBXTargetDependency */, 181 | ); 182 | name = APLoopingScrollViewTests; 183 | productName = APLoopingScrollViewTests; 184 | productReference = FAA3647E1D0D39D900920559 /* APLoopingScrollViewTests.xctest */; 185 | productType = "com.apple.product-type.bundle.unit-test"; 186 | }; 187 | /* End PBXNativeTarget section */ 188 | 189 | /* Begin PBXProject section */ 190 | A75A58E71B8C2D810075BCF8 /* Project object */ = { 191 | isa = PBXProject; 192 | attributes = { 193 | LastSwiftMigration = 0710; 194 | LastSwiftUpdateCheck = 0730; 195 | LastUpgradeCheck = 0710; 196 | ORGANIZATIONNAME = "Andrew Poes"; 197 | TargetAttributes = { 198 | A75A58EE1B8C2D810075BCF8 = { 199 | CreatedOnToolsVersion = 6.4; 200 | DevelopmentTeam = 7JZ3Q8HZ8R; 201 | }; 202 | FAA3647D1D0D39D900920559 = { 203 | CreatedOnToolsVersion = 7.3.1; 204 | TestTargetID = A75A58EE1B8C2D810075BCF8; 205 | }; 206 | }; 207 | }; 208 | buildConfigurationList = A75A58EA1B8C2D810075BCF8 /* Build configuration list for PBXProject "APLoopingScrollView" */; 209 | compatibilityVersion = "Xcode 3.2"; 210 | developmentRegion = English; 211 | hasScannedForEncodings = 0; 212 | knownRegions = ( 213 | en, 214 | Base, 215 | ); 216 | mainGroup = A75A58E61B8C2D810075BCF8; 217 | productRefGroup = A75A58F01B8C2D810075BCF8 /* Products */; 218 | projectDirPath = ""; 219 | projectRoot = ""; 220 | targets = ( 221 | A75A58EE1B8C2D810075BCF8 /* APLoopingScrollView */, 222 | FAA3647D1D0D39D900920559 /* APLoopingScrollViewTests */, 223 | ); 224 | }; 225 | /* End PBXProject section */ 226 | 227 | /* Begin PBXResourcesBuildPhase section */ 228 | A75A58ED1B8C2D810075BCF8 /* Resources */ = { 229 | isa = PBXResourcesBuildPhase; 230 | buildActionMask = 2147483647; 231 | files = ( 232 | A7A346A61BA707D300064306 /* Images.xcassets in Resources */, 233 | A7A346DA1BA7094500064306 /* LaunchScreen.xib in Resources */, 234 | A7A346D81BA7093900064306 /* Storyboard.storyboard in Resources */, 235 | ); 236 | runOnlyForDeploymentPostprocessing = 0; 237 | }; 238 | FAA3647C1D0D39D900920559 /* Resources */ = { 239 | isa = PBXResourcesBuildPhase; 240 | buildActionMask = 2147483647; 241 | files = ( 242 | ); 243 | runOnlyForDeploymentPostprocessing = 0; 244 | }; 245 | /* End PBXResourcesBuildPhase section */ 246 | 247 | /* Begin PBXSourcesBuildPhase section */ 248 | A75A58EB1B8C2D810075BCF8 /* Sources */ = { 249 | isa = PBXSourcesBuildPhase; 250 | buildActionMask = 2147483647; 251 | files = ( 252 | A75A59171B8C2ECC0075BCF8 /* APLoopingScrollView.swift in Sources */, 253 | A75971EF1BA7286E0042D3C4 /* LoopingScrollViewWithScaling.swift in Sources */, 254 | A75A58F71B8C2D810075BCF8 /* ViewController.swift in Sources */, 255 | A75971F11BA762A80042D3C4 /* Examples.swift in Sources */, 256 | A75A58F51B8C2D810075BCF8 /* AppDelegate.swift in Sources */, 257 | ); 258 | runOnlyForDeploymentPostprocessing = 0; 259 | }; 260 | FAA3647A1D0D39D900920559 /* Sources */ = { 261 | isa = PBXSourcesBuildPhase; 262 | buildActionMask = 2147483647; 263 | files = ( 264 | FAA364811D0D39D900920559 /* APLoopingScrollViewTests.swift in Sources */, 265 | ); 266 | runOnlyForDeploymentPostprocessing = 0; 267 | }; 268 | /* End PBXSourcesBuildPhase section */ 269 | 270 | /* Begin PBXTargetDependency section */ 271 | FAA364841D0D39D900920559 /* PBXTargetDependency */ = { 272 | isa = PBXTargetDependency; 273 | target = A75A58EE1B8C2D810075BCF8 /* APLoopingScrollView */; 274 | targetProxy = FAA364831D0D39D900920559 /* PBXContainerItemProxy */; 275 | }; 276 | /* End PBXTargetDependency section */ 277 | 278 | /* Begin XCBuildConfiguration section */ 279 | A75A590C1B8C2D820075BCF8 /* Debug */ = { 280 | isa = XCBuildConfiguration; 281 | buildSettings = { 282 | ALWAYS_SEARCH_USER_PATHS = NO; 283 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 284 | CLANG_CXX_LIBRARY = "libc++"; 285 | CLANG_ENABLE_MODULES = YES; 286 | CLANG_ENABLE_OBJC_ARC = YES; 287 | CLANG_WARN_BOOL_CONVERSION = YES; 288 | CLANG_WARN_CONSTANT_CONVERSION = YES; 289 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 290 | CLANG_WARN_EMPTY_BODY = YES; 291 | CLANG_WARN_ENUM_CONVERSION = YES; 292 | CLANG_WARN_INT_CONVERSION = YES; 293 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 294 | CLANG_WARN_UNREACHABLE_CODE = YES; 295 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 296 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 297 | COPY_PHASE_STRIP = NO; 298 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 299 | ENABLE_STRICT_OBJC_MSGSEND = YES; 300 | ENABLE_TESTABILITY = YES; 301 | GCC_C_LANGUAGE_STANDARD = gnu99; 302 | GCC_DYNAMIC_NO_PIC = NO; 303 | GCC_NO_COMMON_BLOCKS = YES; 304 | GCC_OPTIMIZATION_LEVEL = 0; 305 | GCC_PREPROCESSOR_DEFINITIONS = ( 306 | "DEBUG=1", 307 | "$(inherited)", 308 | ); 309 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 310 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 311 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 312 | GCC_WARN_UNDECLARED_SELECTOR = YES; 313 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 314 | GCC_WARN_UNUSED_FUNCTION = YES; 315 | GCC_WARN_UNUSED_VARIABLE = YES; 316 | IPHONEOS_DEPLOYMENT_TARGET = 8.4; 317 | MTL_ENABLE_DEBUG_INFO = YES; 318 | ONLY_ACTIVE_ARCH = YES; 319 | SDKROOT = iphoneos; 320 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 321 | TARGETED_DEVICE_FAMILY = "1,2"; 322 | }; 323 | name = Debug; 324 | }; 325 | A75A590D1B8C2D820075BCF8 /* Release */ = { 326 | isa = XCBuildConfiguration; 327 | buildSettings = { 328 | ALWAYS_SEARCH_USER_PATHS = NO; 329 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 330 | CLANG_CXX_LIBRARY = "libc++"; 331 | CLANG_ENABLE_MODULES = YES; 332 | CLANG_ENABLE_OBJC_ARC = YES; 333 | CLANG_WARN_BOOL_CONVERSION = YES; 334 | CLANG_WARN_CONSTANT_CONVERSION = YES; 335 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 336 | CLANG_WARN_EMPTY_BODY = YES; 337 | CLANG_WARN_ENUM_CONVERSION = YES; 338 | CLANG_WARN_INT_CONVERSION = YES; 339 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 340 | CLANG_WARN_UNREACHABLE_CODE = YES; 341 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 342 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 343 | COPY_PHASE_STRIP = NO; 344 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 345 | ENABLE_NS_ASSERTIONS = NO; 346 | ENABLE_STRICT_OBJC_MSGSEND = YES; 347 | GCC_C_LANGUAGE_STANDARD = gnu99; 348 | GCC_NO_COMMON_BLOCKS = YES; 349 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 350 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 351 | GCC_WARN_UNDECLARED_SELECTOR = YES; 352 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 353 | GCC_WARN_UNUSED_FUNCTION = YES; 354 | GCC_WARN_UNUSED_VARIABLE = YES; 355 | IPHONEOS_DEPLOYMENT_TARGET = 8.4; 356 | MTL_ENABLE_DEBUG_INFO = NO; 357 | SDKROOT = iphoneos; 358 | TARGETED_DEVICE_FAMILY = "1,2"; 359 | VALIDATE_PRODUCT = YES; 360 | }; 361 | name = Release; 362 | }; 363 | A75A590F1B8C2D820075BCF8 /* Debug */ = { 364 | isa = XCBuildConfiguration; 365 | buildSettings = { 366 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 367 | CODE_SIGN_IDENTITY = "iPhone Developer"; 368 | INFOPLIST_FILE = "$(SRCROOT)/APLoopingScrollView/Info.plist"; 369 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 370 | PRODUCT_BUNDLE_IDENTIFIER = "com.mrbendel.$(PRODUCT_NAME:rfc1034identifier)"; 371 | PRODUCT_NAME = "$(TARGET_NAME)"; 372 | }; 373 | name = Debug; 374 | }; 375 | A75A59101B8C2D820075BCF8 /* Release */ = { 376 | isa = XCBuildConfiguration; 377 | buildSettings = { 378 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 379 | CODE_SIGN_IDENTITY = "iPhone Developer"; 380 | INFOPLIST_FILE = "$(SRCROOT)/APLoopingScrollView/Info.plist"; 381 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 382 | PRODUCT_BUNDLE_IDENTIFIER = "com.mrbendel.$(PRODUCT_NAME:rfc1034identifier)"; 383 | PRODUCT_NAME = "$(TARGET_NAME)"; 384 | }; 385 | name = Release; 386 | }; 387 | FAA364861D0D39D900920559 /* Debug */ = { 388 | isa = XCBuildConfiguration; 389 | buildSettings = { 390 | BUNDLE_LOADER = "$(TEST_HOST)"; 391 | CLANG_ANALYZER_NONNULL = YES; 392 | DEBUG_INFORMATION_FORMAT = dwarf; 393 | INFOPLIST_FILE = APLoopingScrollViewTests/Info.plist; 394 | IPHONEOS_DEPLOYMENT_TARGET = 9.3; 395 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 396 | PRODUCT_BUNDLE_IDENTIFIER = com.andythedesigner.www.APLoopingScrollViewTests; 397 | PRODUCT_NAME = "$(TARGET_NAME)"; 398 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/APLoopingScrollView.app/APLoopingScrollView"; 399 | }; 400 | name = Debug; 401 | }; 402 | FAA364871D0D39D900920559 /* Release */ = { 403 | isa = XCBuildConfiguration; 404 | buildSettings = { 405 | BUNDLE_LOADER = "$(TEST_HOST)"; 406 | CLANG_ANALYZER_NONNULL = YES; 407 | INFOPLIST_FILE = APLoopingScrollViewTests/Info.plist; 408 | IPHONEOS_DEPLOYMENT_TARGET = 9.3; 409 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 410 | PRODUCT_BUNDLE_IDENTIFIER = com.andythedesigner.www.APLoopingScrollViewTests; 411 | PRODUCT_NAME = "$(TARGET_NAME)"; 412 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/APLoopingScrollView.app/APLoopingScrollView"; 413 | }; 414 | name = Release; 415 | }; 416 | /* End XCBuildConfiguration section */ 417 | 418 | /* Begin XCConfigurationList section */ 419 | A75A58EA1B8C2D810075BCF8 /* Build configuration list for PBXProject "APLoopingScrollView" */ = { 420 | isa = XCConfigurationList; 421 | buildConfigurations = ( 422 | A75A590C1B8C2D820075BCF8 /* Debug */, 423 | A75A590D1B8C2D820075BCF8 /* Release */, 424 | ); 425 | defaultConfigurationIsVisible = 0; 426 | defaultConfigurationName = Release; 427 | }; 428 | A75A590E1B8C2D820075BCF8 /* Build configuration list for PBXNativeTarget "APLoopingScrollView" */ = { 429 | isa = XCConfigurationList; 430 | buildConfigurations = ( 431 | A75A590F1B8C2D820075BCF8 /* Debug */, 432 | A75A59101B8C2D820075BCF8 /* Release */, 433 | ); 434 | defaultConfigurationIsVisible = 0; 435 | defaultConfigurationName = Release; 436 | }; 437 | FAA364851D0D39D900920559 /* Build configuration list for PBXNativeTarget "APLoopingScrollViewTests" */ = { 438 | isa = XCConfigurationList; 439 | buildConfigurations = ( 440 | FAA364861D0D39D900920559 /* Debug */, 441 | FAA364871D0D39D900920559 /* Release */, 442 | ); 443 | defaultConfigurationIsVisible = 0; 444 | }; 445 | /* End XCConfigurationList section */ 446 | }; 447 | rootObject = A75A58E71B8C2D810075BCF8 /* Project object */; 448 | } 449 | -------------------------------------------------------------------------------- /APLoopingScrollView/APLoopingScrollView.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /APLoopingScrollView/APLoopingScrollView/Examples/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // APLoopingScrollView 4 | // 5 | // Created by Andrew Poes on 8/25/15. 6 | // Copyright (c) 2015 Andrew Poes. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(application: UIApplication) { 33 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /APLoopingScrollView/APLoopingScrollView/Examples/Examples.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Examples.swift 3 | // APLoopingScrollView 4 | // 5 | // Created by Andrew Poes on 9/14/15. 6 | // Copyright © 2015 Andrew Poes. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ExampleHorz: UIViewController, APLoopingScrollViewDataSource, APLoopingScrollViewDelegate { 12 | 13 | @IBOutlet var backButton: UIButton? 14 | @IBOutlet var loopingScrollView: LoopingScrollViewWithScaling? 15 | 16 | var cacheCount = 0 17 | var cellColors = [UIColor.cyanColor().colorWithAlphaComponent(0.5), UIColor.yellowColor().colorWithAlphaComponent(0.5),UIColor.greenColor().colorWithAlphaComponent(0.5),UIColor.grayColor().colorWithAlphaComponent(0.5)] 18 | 19 | override func viewDidLoad() { 20 | super.viewDidLoad() 21 | 22 | if let loopingScrollView = self.loopingScrollView { 23 | let frame = loopingScrollView.frame 24 | loopingScrollView.backgroundColor = UIColor.whiteColor().colorWithAlphaComponent(0.3) 25 | loopingScrollView.delegate = self 26 | loopingScrollView.dataSource = self 27 | loopingScrollView.scrollDirection = .Horizontal 28 | loopingScrollView.itemSize = CGSize(width: CGRectGetHeight(frame) * 0.5, height: CGRectGetHeight(frame) * 0.5) 29 | loopingScrollView.itemSpacing = 6 30 | loopingScrollView.edgeScale = 0.9 31 | loopingScrollView.pagingEnabled = true 32 | } 33 | } 34 | 35 | @IBAction func handleClearData(sender: UIButton) { 36 | self.cacheCount += 1 37 | self.loopingScrollView?.reloadData() 38 | } 39 | 40 | func loopingScrollViewTotalItems(scrollView: APLoopingScrollView) -> Int { 41 | return 10 42 | } 43 | 44 | func loopingScrollView(scrollView: APLoopingScrollView, viewForIndex index: Int) -> UIView { 45 | let itemSize = scrollView.itemSize 46 | let cell = ExampleCell(frame: CGRect(origin: CGPointZero, size: itemSize)) 47 | cell.text = "\(index)" 48 | let colorIndex = self.cacheCount%self.cellColors.count 49 | cell.backgroundColor = self.cellColors[colorIndex] 50 | return cell 51 | } 52 | 53 | @IBAction func backButtonTouchUpInside(sender: UIButton?) { 54 | self.navigationController?.popViewControllerAnimated(true) 55 | } 56 | } 57 | 58 | class ExampleVert: UIViewController, APLoopingScrollViewDataSource, APLoopingScrollViewDelegate { 59 | 60 | @IBOutlet var backButton: UIButton? 61 | @IBOutlet var loopingScrollView: APLoopingScrollView? 62 | 63 | var cacheCount = 0 64 | var cellColors = [UIColor.cyanColor().colorWithAlphaComponent(0.5), UIColor.yellowColor().colorWithAlphaComponent(0.5),UIColor.greenColor().colorWithAlphaComponent(0.5),UIColor.grayColor().colorWithAlphaComponent(0.5)] 65 | 66 | override func viewDidLoad() { 67 | super.viewDidLoad() 68 | 69 | if let loopingScrollView = self.loopingScrollView { 70 | let frame = loopingScrollView.frame 71 | loopingScrollView.backgroundColor = UIColor.whiteColor().colorWithAlphaComponent(0.3) 72 | loopingScrollView.delegate = self 73 | loopingScrollView.dataSource = self 74 | loopingScrollView.scrollDirection = .Vertical 75 | loopingScrollView.itemSize = CGSize(width: CGRectGetWidth(frame) * 0.5, height: CGRectGetWidth(frame) * 0.5) 76 | loopingScrollView.itemSpacing = 6 77 | loopingScrollView.pagingEnabled = false 78 | } 79 | } 80 | 81 | @IBAction func handleClearData(sender: UIButton) { 82 | self.cacheCount += 1 83 | self.loopingScrollView?.reloadData() 84 | } 85 | 86 | func loopingScrollViewTotalItems(scrollView: APLoopingScrollView) -> Int { 87 | return 10 88 | } 89 | 90 | func loopingScrollView(scrollView: APLoopingScrollView, viewForIndex index: Int) -> UIView { 91 | let itemSize = scrollView.itemSize 92 | let cell = ExampleCell(frame: CGRect(origin: CGPointZero, size: itemSize)) 93 | cell.text = "\(index)" 94 | let colorIndex = self.cacheCount%self.cellColors.count 95 | cell.backgroundColor = self.cellColors[colorIndex] 96 | return cell 97 | } 98 | 99 | @IBAction func backButtonTouchUpInside(sender: UIButton?) { 100 | self.navigationController?.popViewControllerAnimated(true) 101 | } 102 | } 103 | 104 | class ExampleCell: UIView { 105 | var label: UILabel? 106 | 107 | override init(frame: CGRect) { 108 | super.init(frame: frame) 109 | 110 | let label = UILabel(frame: self.bounds) 111 | label.backgroundColor = UIColor.purpleColor().colorWithAlphaComponent(0.5) 112 | label.textColor = UIColor.whiteColor() 113 | label.font = UIFont.systemFontOfSize(48, weight: UIFontWeightBlack) 114 | self.addSubview(label) 115 | self.label = label 116 | } 117 | 118 | required init?(coder aDecoder: NSCoder) { 119 | fatalError("init(coder:) has not been implemented") 120 | } 121 | 122 | var text: String? { 123 | set { 124 | self.label?.text = newValue 125 | self.label?.sizeToFit() 126 | self.label?.center = CGPoint(x: CGRectGetWidth(self.frame) * 0.5, y: CGRectGetHeight(self.frame) * 0.5) 127 | } 128 | get { 129 | return self.label?.text 130 | } 131 | } 132 | } 133 | 134 | -------------------------------------------------------------------------------- /APLoopingScrollView/APLoopingScrollView/Examples/LoopingScrollViewWithScaling.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoopingScrollViewWithScaling.swift 3 | // APLoopingScrollView 4 | // 5 | // Created by Andrew Poes on 9/14/15. 6 | // Copyright © 2015 Andrew Poes. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class LoopingScrollViewWithScaling: APLoopingScrollView { 12 | var edgeScale: CGFloat = 0.9 13 | override func updateViews() { 14 | // reset the transforms 15 | for indexPath in self.visibleItems { 16 | if let view = self.view(forIndexPath: indexPath) { 17 | view.transform = CGAffineTransformIdentity 18 | } 19 | } 20 | // update the frames 21 | super.updateViews() 22 | // set the transforms 23 | let centerX = CGRectGetWidth(self.frame) * 0.5 24 | for indexPath in self.visibleItems { 25 | if let view = self.view(forIndexPath: indexPath) { 26 | let progX = (view.center.x - self.contentOffset.x - centerX) / centerX 27 | let scale = 1 - fabs(progX * (1 - edgeScale)) 28 | let transform = CGAffineTransformMakeScale(scale, scale) 29 | view.transform = transform 30 | } 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /APLoopingScrollView/APLoopingScrollView/Examples/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // APLoopingScrollView 4 | // 5 | // Created by Andrew Poes on 8/25/15. 6 | // Copyright (c) 2015 Andrew Poes. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | } 16 | } 17 | 18 | 19 | -------------------------------------------------------------------------------- /APLoopingScrollView/APLoopingScrollView/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "29x29", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-Small@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "29x29", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-Small@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "40x40", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-40@2x-2.png", 19 | "scale" : "2x" 20 | }, 21 | { 22 | "size" : "40x40", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-40@2x.png", 25 | "scale" : "3x" 26 | }, 27 | { 28 | "size" : "60x60", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-60@2x.png", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "size" : "60x60", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-60@3x.png", 37 | "scale" : "3x" 38 | }, 39 | { 40 | "size" : "29x29", 41 | "idiom" : "ipad", 42 | "filename" : "Icon-Small@2x-1.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "29x29", 47 | "idiom" : "ipad", 48 | "filename" : "Icon-Small@3x-1.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "40x40", 53 | "idiom" : "ipad", 54 | "filename" : "Icon-40-2.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "40x40", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-40@2x-1.png", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "size" : "76x76", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-76.png", 67 | "scale" : "1x" 68 | }, 69 | { 70 | "size" : "76x76", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-76@2x.png", 73 | "scale" : "2x" 74 | } 75 | ], 76 | "info" : { 77 | "version" : 1, 78 | "author" : "xcode" 79 | } 80 | } -------------------------------------------------------------------------------- /APLoopingScrollView/APLoopingScrollView/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Storyboard 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /APLoopingScrollView/APLoopingScrollView/Source/APLoopingScrollView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APLoopingScrollView.swift 3 | // APLoopingScrollView 4 | // 5 | // Created by Andrew Poes on 8/25/15. 6 | // Copyright (c) 2015 Andrew Poes. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | func ==(lhs: APLoopingScrollView.IndexPath, rhs: APLoopingScrollView.IndexPath) -> Bool { 12 | return lhs.item == rhs.item && lhs.offset == rhs.offset 13 | } 14 | 15 | @objc protocol APLoopingScrollViewDataSource: class { 16 | func loopingScrollViewTotalItems(scrollView: APLoopingScrollView) -> Int 17 | func loopingScrollView(scrollView: APLoopingScrollView, viewForIndex index: Int) -> UIView 18 | } 19 | 20 | @objc protocol APLoopingScrollViewDelegate: UIScrollViewDelegate { 21 | optional func loopingScrollView(scrollView: APLoopingScrollView, willDisplayView view: UIView, forItemAtIndex index: Int) 22 | optional func loopingScrollView(scrollView: APLoopingScrollView, didEndDisplayingView view: UIView, forItemAtIndex index: Int) 23 | optional func loopingScrollView(scrollView: APLoopingScrollView, didScrollToIndex index: Int) 24 | } 25 | 26 | class APLoopingScrollView: UIScrollView { 27 | struct IndexPath: Hashable { 28 | var item: Int 29 | var offset: Int 30 | 31 | func description() -> String { 32 | return "\(item):\(offset)" 33 | } 34 | 35 | var hashValue: Int { 36 | get { 37 | return 31 * item + offset 38 | } 39 | } 40 | } 41 | 42 | enum ScrollDirection { 43 | case Horizontal 44 | case Vertical 45 | } 46 | 47 | /// Used to calculate the frame position for element. 48 | enum ItemAlignment { 49 | case PinLeft // Pin element to the left edge. 50 | case PinRight // Pin element to the right edge. 51 | case Centered // Center element on page. 52 | case CenteredLeft // Center element within group to left. 53 | case CenteredPair // Center group of elements. 54 | } 55 | 56 | weak var dataSource: APLoopingScrollViewDataSource? 57 | weak var privateDelegate: APLoopingScrollViewDelegate? { 58 | get { return self.delegate as? APLoopingScrollViewDelegate } 59 | set { self.delegate = newValue } 60 | } 61 | override var delegate: UIScrollViewDelegate? { 62 | set { 63 | if let supportedDelegate = newValue as? APLoopingScrollViewDelegate { 64 | super.delegate = supportedDelegate 65 | } else if newValue != nil { 66 | print("Warning: wrong delegate type set. Should be of type APLoopingScrollViewDelegate") 67 | } 68 | } 69 | get { return super.delegate } 70 | } 71 | 72 | var scrollDirection: ScrollDirection = .Horizontal 73 | var itemSize: CGSize = CGSize() { 74 | didSet { 75 | self.setNeedsUpdateLayoutInfo() 76 | self.setNeedsLayout() 77 | } 78 | } 79 | var itemSpacing: CGFloat = 0 { 80 | didSet { 81 | self.setNeedsUpdateLayoutInfo() 82 | self.setNeedsLayout() 83 | } 84 | } 85 | var itemAlignment: ItemAlignment = .Centered { 86 | didSet { 87 | self.setNeedsUpdateLayoutInfo() 88 | self.setNeedsLayout() 89 | } 90 | } 91 | var currentIndex: Int = 0 92 | var visibleItems = [IndexPath]() 93 | var cachedViews = [String: UIView]() 94 | var needsUpdateLayoutInfo: Bool = true 95 | func setNeedsUpdateLayoutInfo() { 96 | self.needsUpdateLayoutInfo = true 97 | } 98 | 99 | var dragInitialIndex: Int = 0 100 | var displayLink: CADisplayLink? 101 | 102 | struct AnimationProps { 103 | var startValue: CGFloat = 0.0 104 | var endValue: CGFloat = 0.0 105 | var beginTime: CFTimeInterval = 0.0 106 | var animating: Bool = false 107 | var duration: CFTimeInterval = 0.3 108 | 109 | mutating func begin() { 110 | animating = true 111 | beginTime = CACurrentMediaTime() 112 | } 113 | 114 | mutating func reset() { 115 | startValue = 0.0 116 | endValue = 0.0 117 | beginTime = 0.0 118 | animating = false 119 | } 120 | 121 | var delta: CGFloat { 122 | get { 123 | return endValue - startValue 124 | } 125 | } 126 | 127 | mutating func current(progress: CGFloat) -> CGFloat { 128 | return startValue + delta * progress 129 | } 130 | 131 | mutating func wrap(length: CGFloat) { 132 | startValue += length 133 | endValue += length 134 | } 135 | } 136 | var animationProps = AnimationProps() 137 | 138 | // Override pagingEnabled with our own private implementation so that we don't inherit the default 139 | // behavior. We implement custom paging (See animations below). 140 | private var _pagingEnabled: Bool = false { 141 | didSet { 142 | if _pagingEnabled { 143 | // set the scroll speed to be tighter 144 | self.decelerationRate = UIScrollViewDecelerationRateFast 145 | } 146 | } 147 | } 148 | override var pagingEnabled: Bool { 149 | get { 150 | return _pagingEnabled 151 | } 152 | set { 153 | _pagingEnabled = newValue 154 | } 155 | } 156 | 157 | // safe mode function fixes bugs with negative values 158 | static func safemod(a: Int, b: Int) -> Int { 159 | return ((a % b) + b) % b 160 | } 161 | 162 | convenience init(frame: CGRect, scrollDirection: ScrollDirection) { 163 | self.init(frame: frame) 164 | self.scrollDirection = scrollDirection 165 | } 166 | 167 | override init(frame: CGRect) { 168 | super.init(frame: frame) 169 | self.sharedInit() 170 | } 171 | 172 | required init?(coder aDecoder: NSCoder) { 173 | super.init(coder: aDecoder) 174 | self.sharedInit() 175 | } 176 | 177 | func sharedInit() { 178 | self.itemSize = self.frame.size 179 | self.clipsToBounds = true 180 | // Uncomment if you want to see whats happening 181 | // self.showsHorizontalScrollIndicator = false 182 | // self.showsVerticalScrollIndicator = false 183 | 184 | // Listen to pan gesture. 185 | let sel = #selector(APLoopingScrollView.handlePanGesture(_:)) 186 | self.panGestureRecognizer.addTarget(self, action: sel) 187 | } 188 | 189 | func calculatedCenter() -> CGFloat { 190 | let center: CGFloat 191 | if self.scrollDirection == .Vertical { 192 | center = 0.5 * (self.contentSize.height - CGRectGetHeight(self.bounds) - self.itemSpacing) 193 | } else /*if self.scrollDirection == .Horizontal*/ { 194 | center = 0.5 * (self.contentSize.width - CGRectGetWidth(self.bounds) - self.itemSpacing) 195 | } 196 | return center 197 | } 198 | 199 | func updateViewLayoutInfo() { 200 | if self.needsUpdateLayoutInfo { 201 | self.needsUpdateLayoutInfo = false 202 | 203 | let totalItemSpace: CGFloat 204 | let contentSize: CGSize 205 | let edgeInset: CGFloat 206 | let contentInset: UIEdgeInsets 207 | 208 | if self.scrollDirection == .Vertical { 209 | totalItemSpace = itemSize.height + itemSpacing 210 | contentSize = CGSize(width: self.itemSize.width, height: totalItemSpace * 2) 211 | edgeInset = (CGRectGetHeight(self.bounds) - totalItemSpace) * 0.5 212 | contentInset = UIEdgeInsets(top: edgeInset, left: 0, bottom: edgeInset, right: 0) 213 | } else /*if self.scrollDirection == .Horizontal*/ { 214 | totalItemSpace = itemSize.width + itemSpacing 215 | contentSize = CGSize(width: totalItemSpace * 2, height: self.itemSize.height) 216 | edgeInset = (CGRectGetWidth(self.bounds) - totalItemSpace) * 0.5 217 | contentInset = UIEdgeInsets(top: 0, left: edgeInset, bottom: 0, right: edgeInset) 218 | } 219 | 220 | self.contentSize = contentSize 221 | self.contentInset = contentInset 222 | 223 | let centerOffset = self.calculatedCenter() 224 | let contentOffset: CGPoint 225 | if self.scrollDirection == .Vertical { 226 | contentOffset = CGPoint(x: 0, y: centerOffset) 227 | } else /*if self.scrollDirection == .Horizontal*/ { 228 | contentOffset = CGPoint(x: centerOffset, y: 0) 229 | } 230 | self.contentOffset = contentOffset 231 | } 232 | } 233 | 234 | func recenterIfNeccesary() { 235 | let currentOffset: CGFloat 236 | let contentSize: CGFloat 237 | let centerOffset: CGFloat 238 | let distanceFromCenter: CGFloat 239 | let totalItemSpace: CGFloat 240 | let minOffset: CGFloat 241 | let maxOffset: CGFloat 242 | 243 | if self.scrollDirection == .Vertical { 244 | currentOffset = self.contentOffset.y 245 | contentSize = self.contentSize.height 246 | centerOffset = (contentSize - CGRectGetHeight(self.bounds)) * 0.5 247 | distanceFromCenter = fabs(currentOffset - centerOffset) 248 | totalItemSpace = self.itemSize.height + self.itemSpacing 249 | } else /*if self.scrollDirection == .Horizontal*/ { 250 | currentOffset = self.contentOffset.x 251 | contentSize = self.contentSize.width 252 | centerOffset = (contentSize - CGRectGetWidth(self.bounds)) * 0.5 253 | distanceFromCenter = fabs(currentOffset - centerOffset) 254 | totalItemSpace = self.itemSize.width + self.itemSpacing 255 | } 256 | 257 | if distanceFromCenter > totalItemSpace * 0.5 { 258 | if self.scrollDirection == .Vertical { 259 | minOffset = -self.contentInset.top 260 | maxOffset = self.contentSize.height - self.contentInset.bottom - totalItemSpace 261 | } else /*if self.scrollDirection == .Horizontal*/ { 262 | minOffset = -self.contentInset.left 263 | maxOffset = self.contentSize.width - self.contentInset.right - totalItemSpace 264 | } 265 | 266 | let center = self.calculatedCenter() 267 | let indexOffset = currentOffset > center ? 1 : -1 268 | let newIndex = self.currentIndex + indexOffset 269 | self.currentIndex = APLoopingScrollView.safemod(newIndex, b: self.totalItems()) 270 | 271 | let newOffset = indexOffset > 0 ? minOffset : maxOffset 272 | if self.scrollDirection == .Vertical { 273 | self.contentOffset = CGPointMake(self.contentOffset.x, newOffset) 274 | } else /*if self.scrollDirection == .Horizontal*/ { 275 | self.contentOffset = CGPointMake(newOffset, self.contentOffset.y) 276 | } 277 | 278 | // update anim values 279 | if self.animationProps.animating { 280 | let delta = self.animationProps.delta 281 | self.animationProps.startValue = center - delta 282 | self.animationProps.endValue = center 283 | } 284 | } 285 | } 286 | 287 | func updateViews() { 288 | let visibleItems = self.visibleItems(forContentOffset: self.contentOffset) 289 | let hasChanges = self.hasChanges(visibleItems, self.visibleItems) 290 | let newItems = visibleItems.filter { return self.visibleItems.indexOf($0) == nil } 291 | let removedItems = self.visibleItems.filter { return visibleItems.indexOf($0) == nil } 292 | // save the values 293 | self.visibleItems = visibleItems 294 | 295 | for indexPath in visibleItems { 296 | // add any indexes not previously in visibleIndexes 297 | if newItems.indexOf(indexPath) != nil { 298 | if let view = self.view(forIndexPath: indexPath) { 299 | self.privateDelegate?.loopingScrollView?(self, willDisplayView: view, forItemAtIndex: indexPath.item) 300 | self.insertSubview(view, atIndex: 0) 301 | } 302 | } 303 | // if there are changes, update the frames 304 | if hasChanges { 305 | if let view = self.view(forIndexPath: indexPath) { 306 | view.frame = self.frameForView(indexPath.offset) 307 | } 308 | } 309 | } 310 | // remove any indexes not in visible indexes 311 | for indexPath in removedItems { 312 | if let view = self.view(forIndexPath: indexPath) { 313 | view.removeFromSuperview() 314 | self.privateDelegate?.loopingScrollView?(self, didEndDisplayingView: view, forItemAtIndex: indexPath.item) 315 | } 316 | } 317 | } 318 | 319 | // given as offsets from the current index, i.e. [-2, -1, 0, 1, 2] 320 | func visibleItems(forContentOffset contentOffset: CGPoint) -> [IndexPath] { 321 | var visibleItems = [IndexPath]() 322 | 323 | let totalItemSpace: CGFloat 324 | let visibleRect: CGRect 325 | 326 | if self.scrollDirection == .Vertical { 327 | totalItemSpace = self.itemSize.height + self.itemSpacing 328 | visibleRect = CGRect(x: 0, y: self.contentOffset.y, width: CGRectGetWidth(self.bounds), height: totalItemSpace + self.contentInset.top + self.contentInset.bottom) 329 | } else /*if self.scrollDirection == .Horizontal*/ { 330 | totalItemSpace = self.itemSize.width + self.itemSpacing 331 | visibleRect = CGRect(x: self.contentOffset.x, y: 0, width: totalItemSpace + self.contentInset.left + self.contentInset.right, height: CGRectGetHeight(self.bounds)) 332 | } 333 | 334 | let totalItems = self.totalItems() 335 | let minItems = Int(ceil(self.contentSize.width / totalItemSpace)) 336 | let searchLength = max(totalItems, minItems) 337 | // go left 338 | for index in 0 ..< searchLength { 339 | // go left 340 | let frame = self.frameForView(-index) 341 | if CGRectIntersectsRect(frame, visibleRect) { 342 | let item = APLoopingScrollView.safemod(self.currentIndex - index, b: totalItems) 343 | let indexPath = IndexPath(item: item, offset: -index) 344 | visibleItems.append(indexPath) 345 | } 346 | // go right, skipping 0 347 | if index > 0 { 348 | let frame = self.frameForView(index) 349 | if CGRectIntersectsRect(frame, visibleRect) { 350 | let item = APLoopingScrollView.safemod(self.currentIndex + index, b: totalItems) 351 | let indexPath = IndexPath(item: item, offset: index) 352 | visibleItems.append(indexPath) 353 | } 354 | } 355 | } 356 | // sort the offsets so they're in order 357 | visibleItems.sortInPlace { (a, b) -> Bool in 358 | return a.offset < b.offset 359 | } 360 | return visibleItems 361 | } 362 | 363 | func frameForView(distToCenterIndex: Int) -> CGRect { 364 | let totalItemSpace: CGFloat 365 | let screenCenter: CGFloat 366 | 367 | if self.scrollDirection == .Vertical { 368 | totalItemSpace = self.itemSize.height + self.itemSpacing 369 | } else /*if self.scrollDirection == .Horizontal*/ { 370 | totalItemSpace = self.itemSize.width + self.itemSpacing 371 | } 372 | 373 | switch self.itemAlignment { 374 | case .Centered: 375 | screenCenter = totalItemSpace * 0.5 376 | break 377 | case .CenteredLeft: 378 | let totalFit = floor((CGRectGetWidth(self.bounds) - totalItemSpace) * 0.5 / totalItemSpace) 379 | screenCenter = totalItemSpace * 0.5 - totalItemSpace * totalFit 380 | break 381 | case .CenteredPair: 382 | screenCenter = 0 383 | break 384 | case .PinLeft: 385 | screenCenter = -(CGRectGetWidth(self.bounds) - self.itemSpacing) * 0.5 + self.itemSize.width 386 | break 387 | case .PinRight: 388 | screenCenter = (CGRectGetWidth(self.bounds) + self.itemSpacing) * 0.5 389 | break 390 | } 391 | 392 | 393 | let offset = screenCenter + CGFloat(distToCenterIndex) * totalItemSpace 394 | let origin: CGPoint 395 | 396 | if self.scrollDirection == .Vertical { 397 | origin = CGPoint(x: (CGRectGetWidth(self.frame) - self.itemSize.width) * 0.5, y: offset) 398 | } else /*if self.scrollDirection == .Horizontal*/ { 399 | origin = CGPoint(x: offset, y: (CGRectGetHeight(self.frame) - self.itemSize.height) * 0.5) 400 | } 401 | return CGRect(origin: origin, size: self.itemSize) 402 | } 403 | 404 | func hasChanges(a: [IndexPath], _ b: [IndexPath]) -> Bool { 405 | if a.count != b.count { 406 | return true 407 | } else { 408 | for (i, ai) in a.enumerate() { 409 | let bi = b[i] 410 | if ai != bi { 411 | return true 412 | } 413 | } 414 | } 415 | return false 416 | } 417 | 418 | override func layoutSubviews() { 419 | super.layoutSubviews() 420 | // debug logs 421 | // println("x: \(self.contentOffset.x), left: \(self.contentInset.left), right: \(self.contentInset.right)") 422 | 423 | // updates the layout info if neccesary, that is 424 | // contentSize and edge insets to match view width 425 | self.updateViewLayoutInfo() 426 | // check contentOffset and recenter scroll offset 427 | self.recenterIfNeccesary() 428 | // update on screen views 429 | self.updateViews() 430 | } 431 | 432 | // MARK: Datasource protocol 433 | 434 | func totalItems() -> Int { 435 | if let totalItems = self.dataSource?.loopingScrollViewTotalItems(self) { 436 | return totalItems 437 | } 438 | return 0 439 | } 440 | 441 | func view(forIndexPath indexPath: IndexPath) -> UIView? { 442 | let key = indexPath.description() 443 | if self.cachedViews.indexForKey(key) != nil { 444 | return self.cachedViews[key] 445 | } else { 446 | if let view = self.dataSource?.loopingScrollView(self, viewForIndex: indexPath.item) { 447 | self.cachedViews.updateValue(view, forKey: key) 448 | return view 449 | } 450 | } 451 | 452 | return nil 453 | } 454 | 455 | // MARK: UIPanGestureRecognizer 456 | 457 | func handlePanGesture(gestureRecognizer: UIPanGestureRecognizer) { 458 | if self.pagingEnabled { 459 | if gestureRecognizer.state == .Began { 460 | if self.animationProps.animating { 461 | self.stopTargetOffsetAnimation() 462 | } 463 | self.dragInitialIndex = self.currentIndex 464 | } else if gestureRecognizer.state == .Ended || gestureRecognizer.state == .Cancelled { 465 | var targetIndex = 0 466 | 467 | let panVelocity = gestureRecognizer.velocityInView(self) 468 | let activeVelocity: CGFloat 469 | let totalItemSpace: CGFloat 470 | var target: CGFloat 471 | var bounds: CGFloat 472 | 473 | if self.scrollDirection == .Vertical { 474 | activeVelocity = panVelocity.y 475 | bounds = CGRectGetHeight(self.bounds) 476 | totalItemSpace = self.itemSize.height + self.itemSpacing 477 | target = 0.5 * (-bounds + totalItemSpace + self.itemSize.height) 478 | } else /*if self.scrollDirection == .Horizontal*/ { 479 | activeVelocity = panVelocity.x 480 | bounds = CGRectGetWidth(self.bounds) 481 | totalItemSpace = self.itemSize.width + self.itemSpacing 482 | target = 0.5 * (-bounds + totalItemSpace + self.itemSize.width) 483 | } 484 | 485 | if self.currentIndex == self.dragInitialIndex && fabs(activeVelocity) > 10 { 486 | targetIndex = activeVelocity < 0 ? 1 : -1 487 | } 488 | 489 | target += totalItemSpace * CGFloat(targetIndex) 490 | 491 | var duration = CFTimeInterval(fabs(bounds / activeVelocity)) 492 | duration = min(duration, 1.0) 493 | // start the animation 494 | self.startTargetOffsetAnimation(target, duration: duration) 495 | } 496 | } 497 | } 498 | 499 | // MARK: Force-Reload of views 500 | 501 | func reloadData() { 502 | for (_, view) in self.cachedViews { 503 | view.removeFromSuperview() 504 | } 505 | self.visibleItems.removeAll(keepCapacity: true) 506 | self.cachedViews.removeAll(keepCapacity: true) 507 | } 508 | 509 | // MARK: Programatic Animation 510 | 511 | func startTargetOffsetAnimation(target: CGFloat, duration: CFTimeInterval = 0.3) { 512 | self.animationProps.begin() 513 | self.animationProps.startValue = self._getContentOffset() 514 | self.animationProps.endValue = target 515 | self.animationProps.duration = duration 516 | 517 | let sel: Selector = #selector(APLoopingScrollView.animateToTargetOffset(_:)) 518 | self.displayLink = CADisplayLink(target: self, selector: sel) 519 | self.displayLink?.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSRunLoopCommonModes) 520 | } 521 | 522 | func animateToTargetOffset(sender: CADisplayLink) { 523 | let now = CACurrentMediaTime() 524 | let timeDelta = now - self.animationProps.beginTime 525 | let progress = timeDelta / self.animationProps.duration 526 | 527 | if (fabs(progress - 1.0) < DBL_EPSILON || progress > 1.0) { 528 | self._setContentOffset(self.animationProps.endValue) 529 | stopTargetOffsetAnimation() 530 | } else { 531 | // Exponential easing. TODO: Implement customizable easing funcs. 532 | let ease = (progress > 0.0) ? 1 - pow(2, -10 * progress) : progress; 533 | let current = self.animationProps.current(CGFloat(ease)) 534 | self._setContentOffset(current) 535 | } 536 | } 537 | 538 | private func _setContentOffset(offset: CGFloat) { 539 | if self.scrollDirection == .Vertical { 540 | self.setContentOffset(CGPoint(x: self.contentOffset.x, y: offset), animated: false) 541 | } else /*if self.scrollDirection == .Horizontal*/ { 542 | self.setContentOffset(CGPoint(x: offset, y: self.contentOffset.y), animated: false) 543 | } 544 | } 545 | 546 | private func _getContentOffset() -> CGFloat { 547 | if self.scrollDirection == .Vertical { 548 | return self.contentOffset.y 549 | } else /*if self.scrollDirection == .Horizontal*/ { 550 | return self.contentOffset.x 551 | } 552 | } 553 | 554 | func stopTargetOffsetAnimation() { 555 | self.animationProps.reset() 556 | self.displayLink?.invalidate() 557 | self.displayLink = nil 558 | } 559 | } 560 | -------------------------------------------------------------------------------- /APLoopingScrollView/APLoopingScrollView/gifs/horz.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MrBendel/APLoopingScrollView/1a3a63d890e906933035b884839e143a8eabcf87/APLoopingScrollView/APLoopingScrollView/gifs/horz.gif -------------------------------------------------------------------------------- /APLoopingScrollView/APLoopingScrollView/gifs/vert.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MrBendel/APLoopingScrollView/1a3a63d890e906933035b884839e143a8eabcf87/APLoopingScrollView/APLoopingScrollView/gifs/vert.gif -------------------------------------------------------------------------------- /APLoopingScrollView/APLoopingScrollViewTests/APLoopingScrollViewTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APLoopingScrollViewTests.swift 3 | // APLoopingScrollViewTests 4 | // 5 | // Created by apoes on 6/11/16. 6 | // Copyright © 2016 Andrew Poes. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class APLoopingScrollViewTests: XCTestCase { 12 | 13 | override func setUp() { 14 | super.setUp() 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDown() { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | super.tearDown() 21 | } 22 | 23 | func testExample() { 24 | // This is an example of a functional test case. 25 | // Use XCTAssert and related functions to verify your tests produce the correct results. 26 | } 27 | 28 | func testPerformanceExample() { 29 | // This is an example of a performance test case. 30 | self.measureBlock { 31 | // Put the code you want to measure the time of here. 32 | } 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /APLoopingScrollView/APLoopingScrollViewTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /APLoopingScrollView/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "size" : "40x40", 15 | "idiom" : "iphone", 16 | "filename" : "Icon-40@2x.png", 17 | "scale" : "2x" 18 | }, 19 | { 20 | "idiom" : "iphone", 21 | "size" : "40x40", 22 | "scale" : "3x" 23 | }, 24 | { 25 | "size" : "60x60", 26 | "idiom" : "iphone", 27 | "filename" : "Icon-60@2x.png", 28 | "scale" : "2x" 29 | }, 30 | { 31 | "size" : "60x60", 32 | "idiom" : "iphone", 33 | "filename" : "Icon-60@3x.png", 34 | "scale" : "3x" 35 | } 36 | ], 37 | "info" : { 38 | "version" : 1, 39 | "author" : "xcode" 40 | } 41 | } -------------------------------------------------------------------------------- /APLoopingScrollView/Images.xcassets/AppIcon.appiconset/Icon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MrBendel/APLoopingScrollView/1a3a63d890e906933035b884839e143a8eabcf87/APLoopingScrollView/Images.xcassets/AppIcon.appiconset/Icon-40@2x.png -------------------------------------------------------------------------------- /APLoopingScrollView/Images.xcassets/AppIcon.appiconset/Icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MrBendel/APLoopingScrollView/1a3a63d890e906933035b884839e143a8eabcf87/APLoopingScrollView/Images.xcassets/AppIcon.appiconset/Icon-60@2x.png -------------------------------------------------------------------------------- /APLoopingScrollView/Images.xcassets/AppIcon.appiconset/Icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MrBendel/APLoopingScrollView/1a3a63d890e906933035b884839e143a8eabcf87/APLoopingScrollView/Images.xcassets/AppIcon.appiconset/Icon-60@3x.png -------------------------------------------------------------------------------- /APLoopingScrollView/Images.xcassets/iTunesArtwork.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "iTunesArtwork.png" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x", 11 | "filename" : "iTunesArtwork@2x.png" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /APLoopingScrollView/Images.xcassets/iTunesArtwork.imageset/iTunesArtwork.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MrBendel/APLoopingScrollView/1a3a63d890e906933035b884839e143a8eabcf87/APLoopingScrollView/Images.xcassets/iTunesArtwork.imageset/iTunesArtwork.png -------------------------------------------------------------------------------- /APLoopingScrollView/Images.xcassets/iTunesArtwork.imageset/iTunesArtwork@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MrBendel/APLoopingScrollView/1a3a63d890e906933035b884839e143a8eabcf87/APLoopingScrollView/Images.xcassets/iTunesArtwork.imageset/iTunesArtwork@2x.png -------------------------------------------------------------------------------- /APLoopingScrollView/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /APLoopingScrollView/Storyboard.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 | 51 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 186 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 239 | 240 | 241 | 242 | 243 | 244 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Andrew Poes 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # APLoopingScrollView 2 | 3 | ![alt tag](https://raw.githubusercontent.com/MrBendel/APLoopingScrollView/master/APLoopingScrollView/APLoopingScrollView/gifs/horz.gif) ![alt tag](https://raw.githubusercontent.com/MrBendel/APLoopingScrollView/master/APLoopingScrollView/APLoopingScrollView/gifs/vert.gif) 4 | 5 | After failing to find a decent looping scroll view impelementation I set out to build my own. APLoopingScrollView is a direct subclass of `UIScrollView` that displays collections of "cards" in either horizontal or vertical orientation. 6 | 7 | You have control over: 8 | * Item Size 9 | * Item Spacing 10 | * Scroll Direction 11 | * Paging 12 | 13 | APLoopingScrollView supports as few as 1 item and can repeat a single item as needed to fill the screen. The implementation is very simliar to `UICollectionView` in that there is a delegate and datasource. The delegate provides the functionality of UIScrollView mixed with added functionality for the looping ScrollView. The datasource provides the view with the neccesary information it needs to draw such as number of items, and the actual view to draw inside the ScrollView. 14 | 15 | ##How To Use: 16 | 17 | func loopingScrollViewTotalItems(scrollView: APLoopingScrollView) -> Int 18 | 19 | Return the total number of items to display. This controls how many 'cards' appear on screen. 20 | 21 | func loopingScrollView(scrollView: APLoopingScrollView, viewForIndex index: Int) -> UIView 22 | 23 | Return a view for the corrosponding index. *Note, this may be called multiple times for a single index!* If you have less items than needed to display on screen, you'll need to return a view for each instance of a single index, ie. if there's only 1 item to display, return a unique view for each index 0. 24 | 25 | View's are not reused like in a table or collection view, but they are internaly cached. If you need to free the internal cache, call `reloadData` to force reload all visible views. 26 | 27 | ##TODO 28 | 29 | * Logic for handling animation changes for orientation 30 | * Logic for handling insertions or deletions from the view 31 | 32 | Any questions or comments feel free to reach out! 33 | --------------------------------------------------------------------------------