├── .gitignore ├── .swift-version ├── LICENSE ├── README.md ├── VMScrollView.podspec ├── VMScrollView ├── 1.0.0 │ └── VMScrollView.podspec └── VMScrollView.swift ├── VMScrollViewDemo ├── VMScrollViewDemo.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ └── contents.xcworkspacedata └── VMScrollViewDemo │ ├── AppDelegate.swift │ ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ ├── Main.storyboard │ └── VMScrollView.swift │ ├── Info.plist │ ├── ScrollView.swift │ ├── ScrollViewCell.swift │ ├── UIColor+random.swift │ ├── VMScrollView.swift │ └── ViewController.swift └── demo.gif /.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 | .build/ 41 | 42 | # CocoaPods 43 | # 44 | # We recommend against adding the Pods directory to your .gitignore. However 45 | # you should judge for yourself, the pros and cons are mentioned at: 46 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 47 | # 48 | # Pods/ 49 | 50 | # Carthage 51 | # 52 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 53 | # Carthage/Checkouts 54 | 55 | Carthage/Build 56 | 57 | # fastlane 58 | # 59 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 60 | # screenshots whenever they are needed. 61 | # For more information about the recommended setup visit: 62 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 63 | 64 | fastlane/report.xml 65 | fastlane/Preview.html 66 | fastlane/screenshots 67 | fastlane/test_output 68 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 4.0 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Vladimirs Matusevics 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VMScrollView 2 | 3 | ![Demo](http://g.recordit.co/scnobPXDnl.gif) 4 | 5 | ##Installation 6 | 7 | ###CocoaPods 8 | ``` 9 | pod 'VMScrollView' 10 | ``` 11 | 12 | ###Manually 13 | 14 | You can also import **VMScrollView** in to your project manually, just drag and drop VMScrollView.swift file in to your Xcode project. 15 | -------------------------------------------------------------------------------- /VMScrollView.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | 3 | s.name = "VMScrollView" 4 | s.version = "1.0.2" 5 | s.summary = "Infinite horizontal scrolling view with parallax effect." 6 | 7 | s.homepage = "https://github.com/vlondon/VMScrollView" 8 | 9 | s.license = "MIT" 10 | 11 | s.author = { "Vladimirs Matusevics" => "vladimir.matusevic@gmail.com" } 12 | 13 | s.platform = :ios, "10.2" 14 | 15 | s.source = { :git => "https://github.com/vlondon/VMScrollView.git", :tag => "1.0.2" } 16 | 17 | s.source_files = ["VMScrollView/*"] 18 | s.exclude_files = ["VMScrollViewDemo/*"] 19 | 20 | s.pod_target_xcconfig = { 'SWIFT_VERSION' => '3.2' } 21 | 22 | end 23 | -------------------------------------------------------------------------------- /VMScrollView/1.0.0/VMScrollView.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | 3 | s.name = "VMScrollView" 4 | s.version = "1.0.0" 5 | s.summary = "Infinite horizontal scrolling view with parallax effect." 6 | 7 | s.homepage = "https://github.com/vlondon/VMScrollView" 8 | 9 | s.license = "MIT" 10 | 11 | s.author = { "Vladimirs Matusevics" => "vladimir.matusevic@gmail.com" } 12 | 13 | s.platform = :ios, "11.0" 14 | 15 | s.source = { :git => "https://github.com/vlondon/VMScrollView.git", :tag => "1.0.0" } 16 | 17 | s.source_files = ["VMScrollView/*"] 18 | s.exclude_files = ["VMScrollViewDemo/*"] 19 | 20 | s.pod_target_xcconfig = { 'SWIFT_VERSION' => '4.0' } 21 | 22 | end 23 | -------------------------------------------------------------------------------- /VMScrollView/VMScrollView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VMScrollView.swift 3 | // VMScrollView 4 | // 5 | // Created by Vladimirs Matusevics on 2017/09/19. 6 | // Copyright © 2017 Vladimirs Matusevics. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol VMScrollViewProtocol { 12 | func cellClass() -> UICollectionViewCell.Type 13 | 14 | func configureCollectionCell(_ cell: UICollectionViewCell, data: Any) -> UICollectionViewCell 15 | func scrollToPage(_ page: Int) 16 | func scroll(to index: Int, animated: Bool) 17 | func refreshWithNoDataChange() 18 | } 19 | 20 | public protocol VMScrollViewCell { 21 | func updateParallax(for contentOffsetX: CGFloat) 22 | func refresh() 23 | } 24 | 25 | open class VMScrollView: UICollectionReusableView, VMScrollViewProtocol { 26 | 27 | static let kVMScrollCellId = "kVMScrollCellId" 28 | 29 | open func cellClass() -> UICollectionViewCell.Type { 30 | return UICollectionViewCell.self 31 | } 32 | 33 | //MARK:- properties 34 | 35 | public lazy var pageControl: UIPageControl = { 36 | let pageControl = UIPageControl() 37 | pageControl.translatesAutoresizingMaskIntoConstraints = false 38 | self.addSubview(pageControl) 39 | return pageControl 40 | }() 41 | 42 | public var pageControlOffset = UIOffset.zero { 43 | didSet { 44 | self.updatePageControl() 45 | } 46 | } 47 | 48 | public var data: [Any] { 49 | didSet { 50 | if data.count != oldValue.count { 51 | refresh() 52 | } else { 53 | refreshWithNoDataChange() 54 | } 55 | } 56 | } 57 | 58 | public lazy var collectionView: UICollectionView = { 59 | let layout = UICollectionViewFlowLayout() 60 | layout.minimumLineSpacing = 0 61 | layout.minimumLineSpacing = 0 62 | layout.scrollDirection = .horizontal 63 | 64 | let collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: layout) 65 | collectionView.backgroundColor = .clear 66 | collectionView.isPagingEnabled = true 67 | collectionView.delegate = self 68 | collectionView.dataSource = self 69 | collectionView.showsHorizontalScrollIndicator = false 70 | collectionView.showsVerticalScrollIndicator = false 71 | collectionView.bounces = false 72 | 73 | collectionView.register(self.cellClass(), forCellWithReuseIdentifier: VMScrollView.kVMScrollCellId) 74 | return collectionView 75 | }() 76 | 77 | //MARK:- init methods 78 | 79 | public override init(frame: CGRect) { 80 | self.data = [] 81 | super.init(frame: frame) 82 | self.configureConstraints() 83 | } 84 | 85 | public convenience init(with data: [Any] = []) { 86 | self.init(frame: CGRect.zero) 87 | self.data = data 88 | self.refresh() 89 | } 90 | 91 | required public init?(coder aDecoder: NSCoder) { 92 | fatalError("init(coder:) has not been implemented") 93 | } 94 | 95 | private func configureConstraints() { 96 | self.addSubview(collectionView) 97 | collectionView.translatesAutoresizingMaskIntoConstraints = false 98 | let leftConst = NSLayoutConstraint( 99 | item: collectionView, 100 | attribute: .left, 101 | relatedBy: .equal, 102 | toItem: self, 103 | attribute: .left, 104 | multiplier: 1, 105 | constant: 0 106 | ) 107 | let rightConst = NSLayoutConstraint( 108 | item: collectionView, 109 | attribute: .right, 110 | relatedBy: .equal, 111 | toItem: self, 112 | attribute: .right, 113 | multiplier: 1, 114 | constant: 0 115 | ) 116 | let topConst = NSLayoutConstraint( 117 | item: collectionView, 118 | attribute: .top, 119 | relatedBy: .equal, 120 | toItem: self, 121 | attribute: .top, 122 | multiplier: 1, 123 | constant: 0 124 | ) 125 | let bottomConst = NSLayoutConstraint( 126 | item: collectionView, 127 | attribute: .bottom, 128 | relatedBy: .equal, 129 | toItem: self, 130 | attribute: .bottom, 131 | multiplier: 1, 132 | constant: 0 133 | ) 134 | self.addConstraints([leftConst,rightConst,topConst,bottomConst]) 135 | } 136 | 137 | //MARK:- page control setting 138 | 139 | private func removePageControlAutoLayout() { 140 | self.pageControl.removeConstraints(self.pageControl.constraints) 141 | for constraint in self.constraints where constraint.firstItem === self.pageControl { 142 | self.removeConstraint(constraint) 143 | } 144 | } 145 | 146 | fileprivate func calculateParallax() { 147 | collectionView.visibleCells.forEach { cell in 148 | if let vmCell = cell as? VMScrollViewCell, 149 | let convertedPoint = cell.superview?.convert(cell.frame.origin, to: self.superview?.superview) { 150 | vmCell.updateParallax(for: convertedPoint.x) 151 | } 152 | } 153 | } 154 | 155 | public func updatePageControl() { 156 | 157 | if self.data.count <= 1 { 158 | self.updatePageControl(hidden: true) 159 | return 160 | } 161 | 162 | self.updatePageControl(hidden: false) 163 | self.updatePageControl(total: self.data.count) 164 | 165 | let pageControlWidth = Double(self.data.count) * 15 166 | let pageControlHeight = Double(16) 167 | 168 | self.removePageControlAutoLayout() 169 | let widthConst = NSLayoutConstraint( 170 | item: self.pageControl, 171 | attribute: .width, 172 | relatedBy: .equal, 173 | toItem: nil, 174 | attribute: .notAnAttribute, 175 | multiplier: 1, 176 | constant: CGFloat(pageControlWidth) 177 | ) 178 | let hightConst = NSLayoutConstraint( 179 | item: self.pageControl, 180 | attribute: .height, 181 | relatedBy: .equal, 182 | toItem: nil, 183 | attribute: .notAnAttribute, 184 | multiplier: 1, 185 | constant: CGFloat(pageControlHeight) 186 | ) 187 | self.pageControl.addConstraints([widthConst, hightConst]) 188 | 189 | let XConst = NSLayoutConstraint( 190 | item: self.pageControl, 191 | attribute: .centerX, 192 | relatedBy: .equal, 193 | toItem: self, 194 | attribute: .centerX, 195 | multiplier: 1, 196 | constant: pageControlOffset.horizontal 197 | ) 198 | let YConst = NSLayoutConstraint( 199 | item: self.pageControl, 200 | attribute: .bottom, 201 | relatedBy: .equal, 202 | toItem: self, 203 | attribute: .bottom, 204 | multiplier: 1, 205 | constant: pageControlOffset.vertical - 10 206 | ) 207 | self.addConstraints([XConst, YConst]) 208 | } 209 | 210 | open func updatePageControl(hidden: Bool) { 211 | self.pageControl.isHidden = hidden 212 | } 213 | 214 | open func updatePageControl(total pages: Int) { 215 | self.pageControl.numberOfPages = pages 216 | } 217 | 218 | open func updatePageControl(page: Int) { 219 | self.pageControl.currentPage = page 220 | } 221 | 222 | //MARK:- VMScrollViewProtocol 223 | 224 | open func refresh() { 225 | self.collectionView.reloadData() 226 | 227 | if self.data.count > 1 { 228 | // unable to scroll before the collectionview is shown 229 | DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.01) { 230 | self.collectionView.scrollToItem(at: IndexPath(item: 1, section: 0), at: [.top, .left], animated: false) 231 | } 232 | } 233 | 234 | self.updatePageControl() 235 | } 236 | 237 | open func refreshWithNoDataChange() { 238 | self.collectionView.reloadData() 239 | } 240 | 241 | fileprivate func transferIndex(_ index: Int) -> Int { 242 | if self.data.count <= 1 { 243 | return 0 244 | } 245 | 246 | if index == 0 { 247 | return self.data.count - 1 248 | } else if index == self.data.count + 1 { 249 | return 0 250 | } 251 | 252 | return index - 1 253 | } 254 | 255 | open func configureCollectionCell(_ cell: UICollectionViewCell, data: Any) -> UICollectionViewCell { 256 | return cell 257 | } 258 | 259 | open func scrollToPage(_ page: Int) { 260 | self.updatePageControl(page: page) 261 | } 262 | 263 | open func scroll(to index: Int, animated: Bool) { 264 | self.collectionView.scrollToItem(at: IndexPath(item: index, section: 0), at: [.top, .left], animated: animated) 265 | self.scrollToPage(index - 1) 266 | } 267 | 268 | } 269 | 270 | extension VMScrollView: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { 271 | //MARK:- delegate method 272 | 273 | open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 274 | return self.data.count > 1 ? self.data.count + 2 : self.data.count 275 | } 276 | 277 | open func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { 278 | if let cell = cell as? VMScrollViewCell { 279 | cell.refresh() 280 | } 281 | } 282 | 283 | open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 284 | var cell = collectionView.dequeueReusableCell(withReuseIdentifier: VMScrollView.kVMScrollCellId, for: indexPath) 285 | 286 | let index = self.transferIndex(indexPath.row) 287 | 288 | let data = self.data[index] 289 | 290 | cell = self.configureCollectionCell(cell, data: data) 291 | 292 | return cell 293 | 294 | } 295 | 296 | open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 297 | return CGSize(width: collectionView.frame.size.width, height: collectionView.frame.size.height) 298 | } 299 | 300 | open func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 301 | 302 | } 303 | 304 | open func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { 305 | 306 | } 307 | 308 | open func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { 309 | 310 | } 311 | 312 | open func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { 313 | calculateParallax() 314 | } 315 | 316 | open func scrollViewDidScroll(_ scrollView: UIScrollView) { 317 | var index = Float(scrollView.contentOffset.x * 1.0 / scrollView.frame.size.width) 318 | 319 | index = index.isNaN ? 0 : index 320 | 321 | if index < 0.25 { 322 | self.collectionView.scrollToItem(at: IndexPath(item: self.data.count, section: 0), at: [.top,.left], animated: false) 323 | } else if index >= Float(self.data.count + 1) { 324 | self.collectionView.scrollToItem(at: IndexPath(item: 1, section: 0), at: [.top,.left], animated: false) 325 | } 326 | 327 | let page = self.transferIndex(Int(index)) 328 | 329 | scrollToPage(page) 330 | calculateParallax() 331 | } 332 | 333 | } 334 | 335 | -------------------------------------------------------------------------------- /VMScrollViewDemo/VMScrollViewDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 48; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 99B5A6D11F71CA4E0004C385 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99B5A6D01F71CA4E0004C385 /* AppDelegate.swift */; }; 11 | 99B5A6D31F71CA4E0004C385 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99B5A6D21F71CA4E0004C385 /* ViewController.swift */; }; 12 | 99B5A6D61F71CA4E0004C385 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 99B5A6D41F71CA4E0004C385 /* Main.storyboard */; }; 13 | 99B5A6D81F71CA4E0004C385 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 99B5A6D71F71CA4E0004C385 /* Assets.xcassets */; }; 14 | 99B5A6DB1F71CA4E0004C385 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 99B5A6D91F71CA4E0004C385 /* LaunchScreen.storyboard */; }; 15 | 99B5A6E31F71CA6B0004C385 /* VMScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99B5A6E21F71CA6B0004C385 /* VMScrollView.swift */; }; 16 | 99B5A6E51F71CAE40004C385 /* ScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99B5A6E41F71CAE40004C385 /* ScrollView.swift */; }; 17 | 99B5A6E71F71CB890004C385 /* ScrollViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99B5A6E61F71CB890004C385 /* ScrollViewCell.swift */; }; 18 | 99F9ACBC1F7C5B640057A99A /* UIColor+random.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99F9ACBB1F7C5B640057A99A /* UIColor+random.swift */; }; 19 | /* End PBXBuildFile section */ 20 | 21 | /* Begin PBXFileReference section */ 22 | 99B5A6CD1F71CA4E0004C385 /* VMScrollViewDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = VMScrollViewDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 23 | 99B5A6D01F71CA4E0004C385 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 24 | 99B5A6D21F71CA4E0004C385 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 25 | 99B5A6D51F71CA4E0004C385 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 26 | 99B5A6D71F71CA4E0004C385 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 27 | 99B5A6DA1F71CA4E0004C385 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 28 | 99B5A6DC1F71CA4E0004C385 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 29 | 99B5A6E21F71CA6B0004C385 /* VMScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMScrollView.swift; sourceTree = ""; }; 30 | 99B5A6E41F71CAE40004C385 /* ScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollView.swift; sourceTree = ""; }; 31 | 99B5A6E61F71CB890004C385 /* ScrollViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollViewCell.swift; sourceTree = ""; }; 32 | 99F9ACBB1F7C5B640057A99A /* UIColor+random.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+random.swift"; sourceTree = ""; }; 33 | /* End PBXFileReference section */ 34 | 35 | /* Begin PBXFrameworksBuildPhase section */ 36 | 99B5A6CA1F71CA4E0004C385 /* Frameworks */ = { 37 | isa = PBXFrameworksBuildPhase; 38 | buildActionMask = 2147483647; 39 | files = ( 40 | ); 41 | runOnlyForDeploymentPostprocessing = 0; 42 | }; 43 | /* End PBXFrameworksBuildPhase section */ 44 | 45 | /* Begin PBXGroup section */ 46 | 99B5A6C41F71CA4E0004C385 = { 47 | isa = PBXGroup; 48 | children = ( 49 | 99B5A6CF1F71CA4E0004C385 /* VMScrollViewDemo */, 50 | 99B5A6CE1F71CA4E0004C385 /* Products */, 51 | ); 52 | sourceTree = ""; 53 | }; 54 | 99B5A6CE1F71CA4E0004C385 /* Products */ = { 55 | isa = PBXGroup; 56 | children = ( 57 | 99B5A6CD1F71CA4E0004C385 /* VMScrollViewDemo.app */, 58 | ); 59 | name = Products; 60 | sourceTree = ""; 61 | }; 62 | 99B5A6CF1F71CA4E0004C385 /* VMScrollViewDemo */ = { 63 | isa = PBXGroup; 64 | children = ( 65 | 99B5A6D01F71CA4E0004C385 /* AppDelegate.swift */, 66 | 99F9ACBB1F7C5B640057A99A /* UIColor+random.swift */, 67 | 99B5A6D21F71CA4E0004C385 /* ViewController.swift */, 68 | 99B5A6E41F71CAE40004C385 /* ScrollView.swift */, 69 | 99B5A6E61F71CB890004C385 /* ScrollViewCell.swift */, 70 | 99B5A6E21F71CA6B0004C385 /* VMScrollView.swift */, 71 | 99B5A6D41F71CA4E0004C385 /* Main.storyboard */, 72 | 99B5A6D71F71CA4E0004C385 /* Assets.xcassets */, 73 | 99B5A6D91F71CA4E0004C385 /* LaunchScreen.storyboard */, 74 | 99B5A6DC1F71CA4E0004C385 /* Info.plist */, 75 | ); 76 | path = VMScrollViewDemo; 77 | sourceTree = ""; 78 | }; 79 | /* End PBXGroup section */ 80 | 81 | /* Begin PBXNativeTarget section */ 82 | 99B5A6CC1F71CA4E0004C385 /* VMScrollViewDemo */ = { 83 | isa = PBXNativeTarget; 84 | buildConfigurationList = 99B5A6DF1F71CA4E0004C385 /* Build configuration list for PBXNativeTarget "VMScrollViewDemo" */; 85 | buildPhases = ( 86 | 99B5A6C91F71CA4E0004C385 /* Sources */, 87 | 99B5A6CA1F71CA4E0004C385 /* Frameworks */, 88 | 99B5A6CB1F71CA4E0004C385 /* Resources */, 89 | ); 90 | buildRules = ( 91 | ); 92 | dependencies = ( 93 | ); 94 | name = VMScrollViewDemo; 95 | productName = VMScrollViewDemo; 96 | productReference = 99B5A6CD1F71CA4E0004C385 /* VMScrollViewDemo.app */; 97 | productType = "com.apple.product-type.application"; 98 | }; 99 | /* End PBXNativeTarget section */ 100 | 101 | /* Begin PBXProject section */ 102 | 99B5A6C51F71CA4E0004C385 /* Project object */ = { 103 | isa = PBXProject; 104 | attributes = { 105 | LastSwiftUpdateCheck = 0900; 106 | LastUpgradeCheck = 0900; 107 | ORGANIZATIONNAME = vmatusevic; 108 | TargetAttributes = { 109 | 99B5A6CC1F71CA4E0004C385 = { 110 | CreatedOnToolsVersion = 9.0; 111 | ProvisioningStyle = Automatic; 112 | }; 113 | }; 114 | }; 115 | buildConfigurationList = 99B5A6C81F71CA4E0004C385 /* Build configuration list for PBXProject "VMScrollViewDemo" */; 116 | compatibilityVersion = "Xcode 8.0"; 117 | developmentRegion = en; 118 | hasScannedForEncodings = 0; 119 | knownRegions = ( 120 | en, 121 | Base, 122 | ); 123 | mainGroup = 99B5A6C41F71CA4E0004C385; 124 | productRefGroup = 99B5A6CE1F71CA4E0004C385 /* Products */; 125 | projectDirPath = ""; 126 | projectRoot = ""; 127 | targets = ( 128 | 99B5A6CC1F71CA4E0004C385 /* VMScrollViewDemo */, 129 | ); 130 | }; 131 | /* End PBXProject section */ 132 | 133 | /* Begin PBXResourcesBuildPhase section */ 134 | 99B5A6CB1F71CA4E0004C385 /* Resources */ = { 135 | isa = PBXResourcesBuildPhase; 136 | buildActionMask = 2147483647; 137 | files = ( 138 | 99B5A6DB1F71CA4E0004C385 /* LaunchScreen.storyboard in Resources */, 139 | 99B5A6D81F71CA4E0004C385 /* Assets.xcassets in Resources */, 140 | 99B5A6D61F71CA4E0004C385 /* Main.storyboard in Resources */, 141 | ); 142 | runOnlyForDeploymentPostprocessing = 0; 143 | }; 144 | /* End PBXResourcesBuildPhase section */ 145 | 146 | /* Begin PBXSourcesBuildPhase section */ 147 | 99B5A6C91F71CA4E0004C385 /* Sources */ = { 148 | isa = PBXSourcesBuildPhase; 149 | buildActionMask = 2147483647; 150 | files = ( 151 | 99B5A6D31F71CA4E0004C385 /* ViewController.swift in Sources */, 152 | 99B5A6E71F71CB890004C385 /* ScrollViewCell.swift in Sources */, 153 | 99B5A6E51F71CAE40004C385 /* ScrollView.swift in Sources */, 154 | 99B5A6D11F71CA4E0004C385 /* AppDelegate.swift in Sources */, 155 | 99F9ACBC1F7C5B640057A99A /* UIColor+random.swift in Sources */, 156 | 99B5A6E31F71CA6B0004C385 /* VMScrollView.swift in Sources */, 157 | ); 158 | runOnlyForDeploymentPostprocessing = 0; 159 | }; 160 | /* End PBXSourcesBuildPhase section */ 161 | 162 | /* Begin PBXVariantGroup section */ 163 | 99B5A6D41F71CA4E0004C385 /* Main.storyboard */ = { 164 | isa = PBXVariantGroup; 165 | children = ( 166 | 99B5A6D51F71CA4E0004C385 /* Base */, 167 | ); 168 | name = Main.storyboard; 169 | sourceTree = ""; 170 | }; 171 | 99B5A6D91F71CA4E0004C385 /* LaunchScreen.storyboard */ = { 172 | isa = PBXVariantGroup; 173 | children = ( 174 | 99B5A6DA1F71CA4E0004C385 /* Base */, 175 | ); 176 | name = LaunchScreen.storyboard; 177 | sourceTree = ""; 178 | }; 179 | /* End PBXVariantGroup section */ 180 | 181 | /* Begin XCBuildConfiguration section */ 182 | 99B5A6DD1F71CA4E0004C385 /* Debug */ = { 183 | isa = XCBuildConfiguration; 184 | buildSettings = { 185 | ALWAYS_SEARCH_USER_PATHS = NO; 186 | CLANG_ANALYZER_NONNULL = YES; 187 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 188 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 189 | CLANG_CXX_LIBRARY = "libc++"; 190 | CLANG_ENABLE_MODULES = YES; 191 | CLANG_ENABLE_OBJC_ARC = YES; 192 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 193 | CLANG_WARN_BOOL_CONVERSION = YES; 194 | CLANG_WARN_COMMA = YES; 195 | CLANG_WARN_CONSTANT_CONVERSION = YES; 196 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 197 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 198 | CLANG_WARN_EMPTY_BODY = YES; 199 | CLANG_WARN_ENUM_CONVERSION = YES; 200 | CLANG_WARN_INFINITE_RECURSION = YES; 201 | CLANG_WARN_INT_CONVERSION = YES; 202 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 203 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 204 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 205 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 206 | CLANG_WARN_STRICT_PROTOTYPES = YES; 207 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 208 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 209 | CLANG_WARN_UNREACHABLE_CODE = YES; 210 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 211 | CODE_SIGN_IDENTITY = "iPhone Developer"; 212 | COPY_PHASE_STRIP = NO; 213 | DEBUG_INFORMATION_FORMAT = dwarf; 214 | ENABLE_STRICT_OBJC_MSGSEND = YES; 215 | ENABLE_TESTABILITY = YES; 216 | GCC_C_LANGUAGE_STANDARD = gnu11; 217 | GCC_DYNAMIC_NO_PIC = NO; 218 | GCC_NO_COMMON_BLOCKS = YES; 219 | GCC_OPTIMIZATION_LEVEL = 0; 220 | GCC_PREPROCESSOR_DEFINITIONS = ( 221 | "DEBUG=1", 222 | "$(inherited)", 223 | ); 224 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 225 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 226 | GCC_WARN_UNDECLARED_SELECTOR = YES; 227 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 228 | GCC_WARN_UNUSED_FUNCTION = YES; 229 | GCC_WARN_UNUSED_VARIABLE = YES; 230 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 231 | MTL_ENABLE_DEBUG_INFO = YES; 232 | ONLY_ACTIVE_ARCH = YES; 233 | SDKROOT = iphoneos; 234 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 235 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 236 | }; 237 | name = Debug; 238 | }; 239 | 99B5A6DE1F71CA4E0004C385 /* Release */ = { 240 | isa = XCBuildConfiguration; 241 | buildSettings = { 242 | ALWAYS_SEARCH_USER_PATHS = NO; 243 | CLANG_ANALYZER_NONNULL = YES; 244 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 245 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 246 | CLANG_CXX_LIBRARY = "libc++"; 247 | CLANG_ENABLE_MODULES = YES; 248 | CLANG_ENABLE_OBJC_ARC = YES; 249 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 250 | CLANG_WARN_BOOL_CONVERSION = YES; 251 | CLANG_WARN_COMMA = YES; 252 | CLANG_WARN_CONSTANT_CONVERSION = YES; 253 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 254 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 255 | CLANG_WARN_EMPTY_BODY = YES; 256 | CLANG_WARN_ENUM_CONVERSION = YES; 257 | CLANG_WARN_INFINITE_RECURSION = YES; 258 | CLANG_WARN_INT_CONVERSION = YES; 259 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 260 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 261 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 262 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 263 | CLANG_WARN_STRICT_PROTOTYPES = YES; 264 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 265 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 266 | CLANG_WARN_UNREACHABLE_CODE = YES; 267 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 268 | CODE_SIGN_IDENTITY = "iPhone Developer"; 269 | COPY_PHASE_STRIP = NO; 270 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 271 | ENABLE_NS_ASSERTIONS = NO; 272 | ENABLE_STRICT_OBJC_MSGSEND = YES; 273 | GCC_C_LANGUAGE_STANDARD = gnu11; 274 | GCC_NO_COMMON_BLOCKS = YES; 275 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 276 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 277 | GCC_WARN_UNDECLARED_SELECTOR = YES; 278 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 279 | GCC_WARN_UNUSED_FUNCTION = YES; 280 | GCC_WARN_UNUSED_VARIABLE = YES; 281 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 282 | MTL_ENABLE_DEBUG_INFO = NO; 283 | SDKROOT = iphoneos; 284 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 285 | VALIDATE_PRODUCT = YES; 286 | }; 287 | name = Release; 288 | }; 289 | 99B5A6E01F71CA4E0004C385 /* Debug */ = { 290 | isa = XCBuildConfiguration; 291 | buildSettings = { 292 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 293 | CODE_SIGN_STYLE = Automatic; 294 | DEVELOPMENT_TEAM = GVYJ5N3B47; 295 | INFOPLIST_FILE = VMScrollViewDemo/Info.plist; 296 | IPHONEOS_DEPLOYMENT_TARGET = 10.2; 297 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 298 | PRODUCT_BUNDLE_IDENTIFIER = com.vmatusevic.VMScrollViewDemo; 299 | PRODUCT_NAME = "$(TARGET_NAME)"; 300 | SWIFT_VERSION = 3.0; 301 | TARGETED_DEVICE_FAMILY = "1,2"; 302 | }; 303 | name = Debug; 304 | }; 305 | 99B5A6E11F71CA4E0004C385 /* Release */ = { 306 | isa = XCBuildConfiguration; 307 | buildSettings = { 308 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 309 | CODE_SIGN_STYLE = Automatic; 310 | DEVELOPMENT_TEAM = GVYJ5N3B47; 311 | INFOPLIST_FILE = VMScrollViewDemo/Info.plist; 312 | IPHONEOS_DEPLOYMENT_TARGET = 10.2; 313 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 314 | PRODUCT_BUNDLE_IDENTIFIER = com.vmatusevic.VMScrollViewDemo; 315 | PRODUCT_NAME = "$(TARGET_NAME)"; 316 | SWIFT_VERSION = 3.0; 317 | TARGETED_DEVICE_FAMILY = "1,2"; 318 | }; 319 | name = Release; 320 | }; 321 | /* End XCBuildConfiguration section */ 322 | 323 | /* Begin XCConfigurationList section */ 324 | 99B5A6C81F71CA4E0004C385 /* Build configuration list for PBXProject "VMScrollViewDemo" */ = { 325 | isa = XCConfigurationList; 326 | buildConfigurations = ( 327 | 99B5A6DD1F71CA4E0004C385 /* Debug */, 328 | 99B5A6DE1F71CA4E0004C385 /* Release */, 329 | ); 330 | defaultConfigurationIsVisible = 0; 331 | defaultConfigurationName = Release; 332 | }; 333 | 99B5A6DF1F71CA4E0004C385 /* Build configuration list for PBXNativeTarget "VMScrollViewDemo" */ = { 334 | isa = XCConfigurationList; 335 | buildConfigurations = ( 336 | 99B5A6E01F71CA4E0004C385 /* Debug */, 337 | 99B5A6E11F71CA4E0004C385 /* Release */, 338 | ); 339 | defaultConfigurationIsVisible = 0; 340 | defaultConfigurationName = Release; 341 | }; 342 | /* End XCConfigurationList section */ 343 | }; 344 | rootObject = 99B5A6C51F71CA4E0004C385 /* Project object */; 345 | } 346 | -------------------------------------------------------------------------------- /VMScrollViewDemo/VMScrollViewDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /VMScrollViewDemo/VMScrollViewDemo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // VMScrollViewDemo 4 | // 5 | // Created by Vladimirs Matusevics on 19/09/2017. 6 | // Copyright © 2017 vmatusevic. 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: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 17 | return true 18 | } 19 | 20 | func applicationWillResignActive(_ application: UIApplication) {} 21 | 22 | func applicationDidEnterBackground(_ application: UIApplication) {} 23 | 24 | func applicationWillEnterForeground(_ application: UIApplication) {} 25 | 26 | func applicationDidBecomeActive(_ application: UIApplication) {} 27 | 28 | func applicationWillTerminate(_ application: UIApplication) {} 29 | 30 | } 31 | 32 | -------------------------------------------------------------------------------- /VMScrollViewDemo/VMScrollViewDemo/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 | } -------------------------------------------------------------------------------- /VMScrollViewDemo/VMScrollViewDemo/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 | -------------------------------------------------------------------------------- /VMScrollViewDemo/VMScrollViewDemo/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /VMScrollViewDemo/VMScrollViewDemo/Base.lproj/VMScrollView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VMScrollView.swift 3 | // VMScrollView 4 | // 5 | // Created by Vladimirs Matusevics on 2017/09/19. 6 | // Copyright © 2017 Vladimirs Matusevics. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol VMScrollViewProtocol { 12 | var cellClass: UICollectionViewCell.Type { get } 13 | 14 | func configureCollectionCell(_ cell: UICollectionViewCell, data: Any) -> UICollectionViewCell 15 | func scrollToPage(_ page: Int) 16 | func scroll(to index: Int, animated: Bool) 17 | func refreshWithNoDataChange() 18 | } 19 | 20 | public protocol VMScrollViewCell { 21 | func updateParallax(for contentOffsetX: CGFloat) 22 | func refresh() 23 | } 24 | 25 | open class VMScrollView: UICollectionReusableView, VMScrollViewProtocol { 26 | 27 | static let kCYScrollCellId = "kVMScrollCellId" 28 | 29 | open var cellClass: UICollectionViewCell.Type { 30 | return UICollectionViewCell.self 31 | } 32 | 33 | //MARK:- properties 34 | 35 | public lazy var pageControl: UIPageControl = { 36 | let pageControl = UIPageControl() 37 | pageControl.translatesAutoresizingMaskIntoConstraints = false 38 | self.addSubview(pageControl) 39 | return pageControl 40 | }() 41 | 42 | public var pageControlOffset = UIOffset.zero { 43 | didSet { 44 | self.updatePageControl() 45 | } 46 | } 47 | 48 | public var data: [Any] { 49 | didSet { 50 | if data.count != oldValue.count { 51 | refresh() 52 | } else { 53 | refreshWithNoDataChange() 54 | } 55 | } 56 | } 57 | 58 | public lazy var collectionView: UICollectionView = { 59 | let layout = UICollectionViewFlowLayout() 60 | layout.minimumLineSpacing = 0 61 | layout.minimumLineSpacing = 0 62 | layout.scrollDirection = .horizontal 63 | 64 | let collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: layout) 65 | collectionView.backgroundColor = .clear 66 | collectionView.isPagingEnabled = true 67 | collectionView.delegate = self 68 | collectionView.dataSource = self 69 | collectionView.showsHorizontalScrollIndicator = false 70 | collectionView.showsVerticalScrollIndicator = false 71 | collectionView.bounces = false 72 | 73 | collectionView.register(cellClass, forCellWithReuseIdentifier: VMScrollView.kCYScrollCellId) 74 | return collectionView 75 | }() 76 | 77 | //MARK:- init methods 78 | 79 | public override init(frame: CGRect) { 80 | self.data = [] 81 | super.init(frame: frame) 82 | self.configureConstraints() 83 | } 84 | 85 | public convenience init(with data: [Any] = []) { 86 | self.init(frame: CGRect.zero) 87 | self.data = data 88 | self.refresh() 89 | } 90 | 91 | required public init?(coder aDecoder: NSCoder) { 92 | fatalError("init(coder:) has not been implemented") 93 | } 94 | 95 | private func configureConstraints() { 96 | self.addSubview(collectionView) 97 | collectionView.translatesAutoresizingMaskIntoConstraints = false 98 | let leftConst = NSLayoutConstraint( 99 | item: collectionView, 100 | attribute: .left, 101 | relatedBy: .equal, 102 | toItem: self, 103 | attribute: .left, 104 | multiplier: 1, 105 | constant: 0 106 | ) 107 | let rightConst = NSLayoutConstraint( 108 | item: collectionView, 109 | attribute: .right, 110 | relatedBy: .equal, 111 | toItem: self, 112 | attribute: .right, 113 | multiplier: 1, 114 | constant: 0 115 | ) 116 | let topConst = NSLayoutConstraint( 117 | item: collectionView, 118 | attribute: .top, 119 | relatedBy: .equal, 120 | toItem: self, 121 | attribute: .top, 122 | multiplier: 1, 123 | constant: 0 124 | ) 125 | let bottomConst = NSLayoutConstraint( 126 | item: collectionView, 127 | attribute: .bottom, 128 | relatedBy: .equal, 129 | toItem: self, 130 | attribute: .bottom, 131 | multiplier: 1, 132 | constant: 0 133 | ) 134 | self.addConstraints([leftConst,rightConst,topConst,bottomConst]) 135 | } 136 | 137 | //MARK:- page control setting 138 | 139 | private func removePageControlAutoLayout() { 140 | self.pageControl.removeConstraints(self.pageControl.constraints) 141 | for constraint in self.constraints where constraint.firstItem === self.pageControl { 142 | self.removeConstraint(constraint) 143 | } 144 | } 145 | 146 | fileprivate func calculateParallax() { 147 | collectionView.visibleCells.forEach { cell in 148 | if let vmCell = cell as? VMScrollViewCell, 149 | let convertedPoint = cell.superview?.convert(cell.frame.origin, to: self.superview?.superview) { 150 | vmCell.updateParallax(for: convertedPoint.x) 151 | } 152 | } 153 | } 154 | 155 | public func updatePageControl() { 156 | 157 | if self.data.count <= 1 { 158 | self.updatePageControl(hidden: true) 159 | return 160 | } 161 | 162 | self.updatePageControl(hidden: false) 163 | self.updatePageControl(total: self.data.count) 164 | 165 | let pageControlWidth = Double(self.data.count) * 15 166 | let pageControlHeight = Double(16) 167 | 168 | self.removePageControlAutoLayout() 169 | let widthConst = NSLayoutConstraint( 170 | item: self.pageControl, 171 | attribute: .width, 172 | relatedBy: .equal, 173 | toItem: nil, 174 | attribute: .notAnAttribute, 175 | multiplier: 1, 176 | constant: CGFloat(pageControlWidth) 177 | ) 178 | let hightConst = NSLayoutConstraint( 179 | item: self.pageControl, 180 | attribute: .height, 181 | relatedBy: .equal, 182 | toItem: nil, 183 | attribute: .notAnAttribute, 184 | multiplier: 1, 185 | constant: CGFloat(pageControlHeight) 186 | ) 187 | self.pageControl.addConstraints([widthConst, hightConst]) 188 | 189 | let XConst = NSLayoutConstraint( 190 | item: self.pageControl, 191 | attribute: .centerX, 192 | relatedBy: .equal, 193 | toItem: self, 194 | attribute: .centerX, 195 | multiplier: 1, 196 | constant: pageControlOffset.horizontal 197 | ) 198 | let YConst = NSLayoutConstraint( 199 | item: self.pageControl, 200 | attribute: .bottom, 201 | relatedBy: .equal, 202 | toItem: self, 203 | attribute: .bottom, 204 | multiplier: 1, 205 | constant: pageControlOffset.vertical - 10 206 | ) 207 | self.addConstraints([XConst, YConst]) 208 | } 209 | 210 | open func updatePageControl(hidden: Bool) { 211 | self.pageControl.isHidden = hidden 212 | } 213 | 214 | open func updatePageControl(total pages: Int) { 215 | self.pageControl.numberOfPages = pages 216 | } 217 | 218 | open func updatePageControl(page: Int) { 219 | self.pageControl.currentPage = page 220 | } 221 | 222 | //MARK:- VMScrollViewProtocol 223 | 224 | open func refresh() { 225 | self.collectionView.reloadData() 226 | 227 | if self.data.count > 1 { 228 | // unable to scroll before the collectionview is shown 229 | DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.01) { 230 | self.collectionView.scrollToItem(at: IndexPath(item: 1, section: 0), at: [.top, .left], animated: false) 231 | } 232 | } 233 | 234 | self.updatePageControl() 235 | } 236 | 237 | open func refreshWithNoDataChange() { 238 | self.collectionView.reloadData() 239 | } 240 | 241 | fileprivate func transferIndex(_ index: Int) -> Int { 242 | if self.data.count <= 1 { 243 | return 0 244 | } 245 | 246 | if index == 0 { 247 | return self.data.count - 1 248 | } else if index == self.data.count + 1 { 249 | return 0 250 | } 251 | 252 | return index - 1 253 | } 254 | 255 | open func configureCollectionCell(_ cell: UICollectionViewCell, data: Any) -> UICollectionViewCell { 256 | return cell 257 | } 258 | 259 | open func scrollToPage(_ page: Int) { 260 | self.updatePageControl(page: page) 261 | } 262 | 263 | open func scroll(to index: Int, animated: Bool) { 264 | self.collectionView.scrollToItem(at: IndexPath(item: index, section: 0), at: [.top, .left], animated: animated) 265 | self.scrollToPage(index - 1) 266 | } 267 | 268 | } 269 | 270 | extension VMScrollView: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { 271 | //MARK:- delegate method 272 | 273 | open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 274 | return self.data.count > 1 ? self.data.count + 2 : self.data.count 275 | } 276 | 277 | open func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { 278 | if let cell = cell as? VMScrollViewCell { 279 | cell.refresh() 280 | } 281 | } 282 | 283 | open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 284 | var cell = collectionView.dequeueReusableCell(withReuseIdentifier: VMScrollView.kCYScrollCellId, for: indexPath) 285 | 286 | let index = self.transferIndex(indexPath.row) 287 | 288 | let data = self.data[index] 289 | 290 | cell = self.configureCollectionCell(cell, data: data) 291 | 292 | return cell 293 | 294 | } 295 | 296 | open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 297 | return CGSize(width: collectionView.frame.size.width, height: collectionView.frame.size.height) 298 | } 299 | 300 | open func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 301 | 302 | } 303 | 304 | open func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { 305 | 306 | } 307 | 308 | open func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { 309 | 310 | } 311 | 312 | open func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { 313 | calculateParallax() 314 | } 315 | 316 | open func scrollViewDidScroll(_ scrollView: UIScrollView) { 317 | var index = Float(scrollView.contentOffset.x * 1.0 / scrollView.frame.size.width) 318 | 319 | index = index.isNaN ? 0 : index 320 | 321 | if index < 0.25 { 322 | self.collectionView.scrollToItem(at: IndexPath(item: self.data.count, section: 0), at: [.top,.left], animated: false) 323 | } else if index >= Float(self.data.count + 1) { 324 | self.collectionView.scrollToItem(at: IndexPath(item: 1, section: 0), at: [.top,.left], animated: false) 325 | } 326 | 327 | let page = self.transferIndex(Int(index)) 328 | 329 | scrollToPage(page) 330 | calculateParallax() 331 | } 332 | 333 | } 334 | 335 | -------------------------------------------------------------------------------- /VMScrollViewDemo/VMScrollViewDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 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 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /VMScrollViewDemo/VMScrollViewDemo/ScrollView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScrollView.swift 3 | // VMScrollViewDemo 4 | // 5 | // Created by Vladimirs Matusevics on 19/09/2017. 6 | // Copyright © 2017 vmatusevic. All rights reserved. 7 | // 8 | 9 | 10 | import UIKit 11 | 12 | class ScrollView: VMScrollView { 13 | 14 | override func cellClass() -> UICollectionViewCell.Type { 15 | return ScrollViewCell.self 16 | } 17 | 18 | override func configureCollectionCell(_ cell: UICollectionViewCell, data: Any) -> UICollectionViewCell { 19 | let cell = cell as! ScrollViewCell 20 | 21 | if let data = data as? CellData { 22 | cell.update(with: data) 23 | } 24 | 25 | return cell 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /VMScrollViewDemo/VMScrollViewDemo/ScrollViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScrollViewCell.swift 3 | // VMScrollViewDemo 4 | // 5 | // Created by Vladimirs Matusevics on 19/09/2017. 6 | // Copyright © 2017 vmatusevic. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | typealias ParallaxConstraint = (constraint: NSLayoutConstraint, multiplier: CGFloat) 12 | 13 | struct CellData { 14 | var name: String 15 | var descr: String 16 | var color: UIColor 17 | } 18 | 19 | class ScrollViewCell: UICollectionViewCell { 20 | 21 | private var subviewsDidLayout = false 22 | 23 | private lazy var titleLabel: UILabel = { 24 | let label = UILabel() 25 | label.numberOfLines = 0 26 | label.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .title1), size: 0) 27 | label.textColor = .white 28 | return label 29 | }() 30 | 31 | private lazy var bodyLabel: UILabel = { 32 | let label = UILabel() 33 | label.numberOfLines = 0 34 | label.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .body), size: 0) 35 | label.textColor = .white 36 | return label 37 | }() 38 | 39 | fileprivate lazy var titleCenterConst: ParallaxConstraint = { 40 | return (NSLayoutConstraint( 41 | item: self.titleLabel, 42 | attribute: .centerX, 43 | relatedBy: .equal, 44 | toItem: self.contentView, 45 | attribute: .centerX, 46 | multiplier: 1, 47 | constant: 0 48 | ), 0.3) 49 | }() 50 | 51 | fileprivate lazy var bodyCenterConst: ParallaxConstraint = { 52 | return (NSLayoutConstraint( 53 | item: self.bodyLabel, 54 | attribute: .centerX, 55 | relatedBy: .equal, 56 | toItem: self.contentView, 57 | attribute: .centerX, 58 | multiplier: 1, 59 | constant: 0 60 | ), 0.03) 61 | }() 62 | 63 | fileprivate lazy var bodyTopConst: ParallaxConstraint = { 64 | return (NSLayoutConstraint( 65 | item: self.bodyLabel, 66 | attribute: .top, 67 | relatedBy: .equal, 68 | toItem: self.titleLabel, 69 | attribute: .bottom, 70 | multiplier: 1, 71 | constant: 0 72 | ), 0.2) 73 | }() 74 | 75 | override init(frame: CGRect) { 76 | super.init(frame: frame) 77 | 78 | self.contentView.translatesAutoresizingMaskIntoConstraints = true 79 | titleLabel.translatesAutoresizingMaskIntoConstraints = false 80 | bodyLabel.translatesAutoresizingMaskIntoConstraints = false 81 | 82 | self.contentView.addSubview(titleLabel) 83 | self.contentView.addSubview(bodyLabel) 84 | } 85 | 86 | required init?(coder aDecoder: NSCoder) { 87 | fatalError("init(coder:) has not been implemented") 88 | } 89 | 90 | override func layoutSubviews() { 91 | contentView.frame = bounds 92 | 93 | if !self.subviewsDidLayout { 94 | 95 | let titleTopConst = NSLayoutConstraint( 96 | item: titleLabel, 97 | attribute: .top, 98 | relatedBy: .equal, 99 | toItem: self.contentView, 100 | attribute: .top, 101 | multiplier: 1, 102 | constant: 40 103 | ) 104 | 105 | self.contentView.addConstraints([ 106 | titleCenterConst.constraint, 107 | titleTopConst, 108 | bodyCenterConst.constraint, 109 | bodyTopConst.constraint 110 | ]) 111 | 112 | self.subviewsDidLayout = true 113 | } 114 | 115 | super.layoutSubviews() 116 | } 117 | 118 | func update(with data: CellData) { 119 | self.contentView.backgroundColor = data.color 120 | titleLabel.text = data.name 121 | bodyLabel.text = data.descr 122 | } 123 | 124 | } 125 | 126 | extension ScrollViewCell: VMScrollViewCell { 127 | 128 | func updateParallax(for contentOffsetX: CGFloat) { 129 | titleCenterConst.constraint.constant = contentOffsetX * titleCenterConst.multiplier 130 | bodyCenterConst.constraint.constant = contentOffsetX * bodyCenterConst.multiplier 131 | bodyTopConst.constraint.constant = abs(contentOffsetX) * bodyTopConst.multiplier 132 | } 133 | 134 | func refresh() { 135 | titleCenterConst.constraint.constant = 0 136 | bodyCenterConst.constraint.constant = 0 137 | bodyTopConst.constraint.constant = 0 138 | } 139 | 140 | } 141 | -------------------------------------------------------------------------------- /VMScrollViewDemo/VMScrollViewDemo/UIColor+random.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+random.swift 3 | // VMScrollViewDemo 4 | // 5 | // Created by Vladimirs Matusevics on 27/09/2017. 6 | // Copyright © 2017 vmatusevic. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIColor { 12 | 13 | static func random() -> UIColor { 14 | let randomRed = CGFloat(arc4random()) / CGFloat(UInt32.max) 15 | let randomGreen = CGFloat(arc4random()) / CGFloat(UInt32.max) 16 | let randomBlue = CGFloat(arc4random()) / CGFloat(UInt32.max) 17 | 18 | return UIColor(red: randomRed, green: randomGreen, blue: randomBlue, alpha: 1.0) 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /VMScrollViewDemo/VMScrollViewDemo/VMScrollView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VMScrollView.swift 3 | // VMScrollView 4 | // 5 | // Created by Vladimirs Matusevics on 2017/09/19. 6 | // Copyright © 2017 Vladimirs Matusevics. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol VMScrollViewProtocol { 12 | func cellClass() -> UICollectionViewCell.Type 13 | 14 | func configureCollectionCell(_ cell: UICollectionViewCell, data: Any) -> UICollectionViewCell 15 | func scrollToPage(_ page: Int) 16 | func scroll(to index: Int, animated: Bool) 17 | func refreshWithNoDataChange() 18 | } 19 | 20 | public protocol VMScrollViewCell { 21 | func updateParallax(for contentOffsetX: CGFloat) 22 | func refresh() 23 | } 24 | 25 | open class VMScrollView: UICollectionReusableView, VMScrollViewProtocol { 26 | 27 | static let kVMScrollCellId = "kVMScrollCellId" 28 | 29 | open func cellClass() -> UICollectionViewCell.Type { 30 | return UICollectionViewCell.self 31 | } 32 | 33 | //MARK:- properties 34 | 35 | public lazy var pageControl: UIPageControl = { 36 | let pageControl = UIPageControl() 37 | pageControl.translatesAutoresizingMaskIntoConstraints = false 38 | self.addSubview(pageControl) 39 | return pageControl 40 | }() 41 | 42 | public var pageControlOffset = UIOffset.zero { 43 | didSet { 44 | self.updatePageControl() 45 | } 46 | } 47 | 48 | public var data: [Any] { 49 | didSet { 50 | if data.count != oldValue.count { 51 | refresh() 52 | } else { 53 | refreshWithNoDataChange() 54 | } 55 | } 56 | } 57 | 58 | public lazy var collectionView: UICollectionView = { 59 | let layout = UICollectionViewFlowLayout() 60 | layout.minimumLineSpacing = 0 61 | layout.minimumLineSpacing = 0 62 | layout.scrollDirection = .horizontal 63 | 64 | let collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: layout) 65 | collectionView.backgroundColor = .clear 66 | collectionView.isPagingEnabled = true 67 | collectionView.delegate = self 68 | collectionView.dataSource = self 69 | collectionView.showsHorizontalScrollIndicator = false 70 | collectionView.showsVerticalScrollIndicator = false 71 | collectionView.bounces = false 72 | 73 | collectionView.register(self.cellClass(), forCellWithReuseIdentifier: VMScrollView.kVMScrollCellId) 74 | return collectionView 75 | }() 76 | 77 | //MARK:- init methods 78 | 79 | public override init(frame: CGRect) { 80 | self.data = [] 81 | super.init(frame: frame) 82 | self.configureConstraints() 83 | } 84 | 85 | public convenience init(with data: [Any] = []) { 86 | self.init(frame: CGRect.zero) 87 | self.data = data 88 | self.refresh() 89 | } 90 | 91 | required public init?(coder aDecoder: NSCoder) { 92 | fatalError("init(coder:) has not been implemented") 93 | } 94 | 95 | private func configureConstraints() { 96 | self.addSubview(collectionView) 97 | collectionView.translatesAutoresizingMaskIntoConstraints = false 98 | let leftConst = NSLayoutConstraint( 99 | item: collectionView, 100 | attribute: .left, 101 | relatedBy: .equal, 102 | toItem: self, 103 | attribute: .left, 104 | multiplier: 1, 105 | constant: 0 106 | ) 107 | let rightConst = NSLayoutConstraint( 108 | item: collectionView, 109 | attribute: .right, 110 | relatedBy: .equal, 111 | toItem: self, 112 | attribute: .right, 113 | multiplier: 1, 114 | constant: 0 115 | ) 116 | let topConst = NSLayoutConstraint( 117 | item: collectionView, 118 | attribute: .top, 119 | relatedBy: .equal, 120 | toItem: self, 121 | attribute: .top, 122 | multiplier: 1, 123 | constant: 0 124 | ) 125 | let bottomConst = NSLayoutConstraint( 126 | item: collectionView, 127 | attribute: .bottom, 128 | relatedBy: .equal, 129 | toItem: self, 130 | attribute: .bottom, 131 | multiplier: 1, 132 | constant: 0 133 | ) 134 | self.addConstraints([leftConst,rightConst,topConst,bottomConst]) 135 | } 136 | 137 | //MARK:- page control setting 138 | 139 | private func removePageControlAutoLayout() { 140 | self.pageControl.removeConstraints(self.pageControl.constraints) 141 | for constraint in self.constraints where constraint.firstItem === self.pageControl { 142 | self.removeConstraint(constraint) 143 | } 144 | } 145 | 146 | fileprivate func calculateParallax() { 147 | collectionView.visibleCells.forEach { cell in 148 | if let vmCell = cell as? VMScrollViewCell, 149 | let convertedPoint = cell.superview?.convert(cell.frame.origin, to: self.superview?.superview) { 150 | vmCell.updateParallax(for: convertedPoint.x) 151 | } 152 | } 153 | } 154 | 155 | public func updatePageControl() { 156 | 157 | if self.data.count <= 1 { 158 | self.updatePageControl(hidden: true) 159 | return 160 | } 161 | 162 | self.updatePageControl(hidden: false) 163 | self.updatePageControl(total: self.data.count) 164 | 165 | let pageControlWidth = Double(self.data.count) * 15 166 | let pageControlHeight = Double(16) 167 | 168 | self.removePageControlAutoLayout() 169 | let widthConst = NSLayoutConstraint( 170 | item: self.pageControl, 171 | attribute: .width, 172 | relatedBy: .equal, 173 | toItem: nil, 174 | attribute: .notAnAttribute, 175 | multiplier: 1, 176 | constant: CGFloat(pageControlWidth) 177 | ) 178 | let hightConst = NSLayoutConstraint( 179 | item: self.pageControl, 180 | attribute: .height, 181 | relatedBy: .equal, 182 | toItem: nil, 183 | attribute: .notAnAttribute, 184 | multiplier: 1, 185 | constant: CGFloat(pageControlHeight) 186 | ) 187 | self.pageControl.addConstraints([widthConst, hightConst]) 188 | 189 | let XConst = NSLayoutConstraint( 190 | item: self.pageControl, 191 | attribute: .centerX, 192 | relatedBy: .equal, 193 | toItem: self, 194 | attribute: .centerX, 195 | multiplier: 1, 196 | constant: pageControlOffset.horizontal 197 | ) 198 | let YConst = NSLayoutConstraint( 199 | item: self.pageControl, 200 | attribute: .bottom, 201 | relatedBy: .equal, 202 | toItem: self, 203 | attribute: .bottom, 204 | multiplier: 1, 205 | constant: pageControlOffset.vertical - 10 206 | ) 207 | self.addConstraints([XConst, YConst]) 208 | } 209 | 210 | open func updatePageControl(hidden: Bool) { 211 | self.pageControl.isHidden = hidden 212 | } 213 | 214 | open func updatePageControl(total pages: Int) { 215 | self.pageControl.numberOfPages = pages 216 | } 217 | 218 | open func updatePageControl(page: Int) { 219 | self.pageControl.currentPage = page 220 | } 221 | 222 | //MARK:- VMScrollViewProtocol 223 | 224 | open func refresh() { 225 | self.collectionView.reloadData() 226 | 227 | if self.data.count > 1 { 228 | // unable to scroll before the collectionview is shown 229 | DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.01) { 230 | self.collectionView.scrollToItem(at: IndexPath(item: 1, section: 0), at: [.top, .left], animated: false) 231 | } 232 | } 233 | 234 | self.updatePageControl() 235 | } 236 | 237 | open func refreshWithNoDataChange() { 238 | self.collectionView.reloadData() 239 | } 240 | 241 | fileprivate func transferIndex(_ index: Int) -> Int { 242 | if self.data.count <= 1 { 243 | return 0 244 | } 245 | 246 | if index == 0 { 247 | return self.data.count - 1 248 | } else if index == self.data.count + 1 { 249 | return 0 250 | } 251 | 252 | return index - 1 253 | } 254 | 255 | open func configureCollectionCell(_ cell: UICollectionViewCell, data: Any) -> UICollectionViewCell { 256 | return cell 257 | } 258 | 259 | open func scrollToPage(_ page: Int) { 260 | self.updatePageControl(page: page) 261 | } 262 | 263 | open func scroll(to index: Int, animated: Bool) { 264 | self.collectionView.scrollToItem(at: IndexPath(item: index, section: 0), at: [.top, .left], animated: animated) 265 | self.scrollToPage(index - 1) 266 | } 267 | 268 | } 269 | 270 | extension VMScrollView: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { 271 | //MARK:- delegate method 272 | 273 | open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 274 | return self.data.count > 1 ? self.data.count + 2 : self.data.count 275 | } 276 | 277 | open func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { 278 | if let cell = cell as? VMScrollViewCell { 279 | cell.refresh() 280 | } 281 | } 282 | 283 | open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 284 | var cell = collectionView.dequeueReusableCell(withReuseIdentifier: VMScrollView.kVMScrollCellId, for: indexPath) 285 | 286 | let index = self.transferIndex(indexPath.row) 287 | 288 | let data = self.data[index] 289 | 290 | cell = self.configureCollectionCell(cell, data: data) 291 | 292 | return cell 293 | 294 | } 295 | 296 | open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 297 | return CGSize(width: collectionView.frame.size.width, height: collectionView.frame.size.height) 298 | } 299 | 300 | open func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 301 | 302 | } 303 | 304 | open func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { 305 | 306 | } 307 | 308 | open func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { 309 | 310 | } 311 | 312 | open func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { 313 | calculateParallax() 314 | } 315 | 316 | open func scrollViewDidScroll(_ scrollView: UIScrollView) { 317 | var index = Float(scrollView.contentOffset.x * 1.0 / scrollView.frame.size.width) 318 | 319 | index = index.isNaN ? 0 : index 320 | 321 | if index < 0.25 { 322 | self.collectionView.scrollToItem(at: IndexPath(item: self.data.count, section: 0), at: [.top,.left], animated: false) 323 | } else if index >= Float(self.data.count + 1) { 324 | self.collectionView.scrollToItem(at: IndexPath(item: 1, section: 0), at: [.top,.left], animated: false) 325 | } 326 | 327 | let page = self.transferIndex(Int(index)) 328 | 329 | scrollToPage(page) 330 | calculateParallax() 331 | } 332 | 333 | } 334 | 335 | -------------------------------------------------------------------------------- /VMScrollViewDemo/VMScrollViewDemo/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // VMScrollViewDemo 4 | // 5 | // Created by Vladimirs Matusevics on 19/09/2017. 6 | // Copyright © 2017 vmatusevic. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | private lazy var scrollView: ScrollView = { 14 | return ScrollView() 15 | }() 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | 20 | addSubviews() 21 | 22 | scrollView.data = [ 23 | CellData(name: "First Cell", descr: "Aaaaa aaa aaa", color: .random()), 24 | CellData(name: "Second Cell", descr: "Bbb bbbbb bbbbb", color: .random()), 25 | CellData(name: "Third Cell", descr: "Cccc cccccc ccccc", color: .random()) 26 | ] 27 | } 28 | 29 | private func addSubviews() { 30 | scrollView.translatesAutoresizingMaskIntoConstraints = false 31 | view.addSubview(scrollView) 32 | 33 | let top = NSLayoutConstraint( 34 | item: scrollView, 35 | attribute: .top, 36 | relatedBy: .equal, 37 | toItem: self.view, 38 | attribute: .top, 39 | multiplier: 1, 40 | constant: 0 41 | ) 42 | 43 | let right = NSLayoutConstraint( 44 | item: scrollView, 45 | attribute: .right, 46 | relatedBy: .equal, 47 | toItem: self.view, 48 | attribute: .right, 49 | multiplier: 1, 50 | constant: 0 51 | ) 52 | 53 | let bottom = NSLayoutConstraint( 54 | item: scrollView, 55 | attribute: .bottom, 56 | relatedBy: .equal, 57 | toItem: self.view, 58 | attribute: .bottom, 59 | multiplier: 1, 60 | constant: 0 61 | ) 62 | 63 | let left = NSLayoutConstraint( 64 | item: scrollView, 65 | attribute: .left, 66 | relatedBy: .equal, 67 | toItem: self.view, 68 | attribute: .left, 69 | multiplier: 1, 70 | constant: 0 71 | ) 72 | 73 | view.addConstraints([top, right, bottom, left]) 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlondon/VMScrollView/fce25a5e6194bf02703667108450786b9e42fd0e/demo.gif --------------------------------------------------------------------------------