├── .swift-version ├── .gitignore ├── LTInfiniteScrollView-Swift.xcodeproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── project.pbxproj ├── LTInfiniteScrollView-Swift ├── Lauch.storyboard ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Info.plist ├── AppDelegate.swift ├── ViewController.swift ├── Base.lproj │ └── Main.storyboard └── Sources │ └── LTInfiniteScrollView.swift ├── LTInfiniteScrollViewSwift.podspec ├── LICENSE └── README.md /.swift-version: -------------------------------------------------------------------------------- 1 | 3.0 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.swp 3 | *~.nib 4 | Pods 5 | build/ 6 | xcuserdata/ 7 | *.pbxuser 8 | *.perspective 9 | *.perspectivev3 10 | -------------------------------------------------------------------------------- /LTInfiniteScrollView-Swift.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /LTInfiniteScrollView-Swift/Lauch.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /LTInfiniteScrollViewSwift.podspec: -------------------------------------------------------------------------------- 1 | 2 | Pod::Spec.new do |s| 3 | s.name = "LTInfiniteScrollViewSwift" 4 | s.version = "0.5.0" 5 | s.summary = "An infinite scrollview allowing easily applying animation" 6 | s.homepage = "https://github.com/ltebean/LTInfiniteScrollView-Swift" 7 | s.license = "MIT" 8 | s.author = { "ltebean" => "yucong1118@gmail.com" } 9 | s.source = { :git => "https://github.com/ltebean/LTInfiniteScrollView-Swift.git", :tag => 'v0.5.0'} 10 | s.source_files = "LTInfiniteScrollView-Swift/Sources/LTInfiniteScrollView.swift" 11 | s.requires_arc = true 12 | s.platform = :ios, '8.0' 13 | 14 | end 15 | -------------------------------------------------------------------------------- /LTInfiniteScrollView-Swift/Assets.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 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2015 ltebean 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | (MIT License) -------------------------------------------------------------------------------- /LTInfiniteScrollView-Swift/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 | Lauch 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /LTInfiniteScrollView-Swift/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // LTInfiniteScrollView-Swift 4 | // 5 | // Created by ltebean on 16/1/8. 6 | // Copyright © 2016年 io. 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: [UIApplicationLaunchOptionsKey: Any]?) -> 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![LTInfiniteScrollViewSwift](https://cocoapod-badges.herokuapp.com/v/LTInfiniteScrollViewSwift/badge.png) 2 | 3 | ## Demo 4 | ##### 1. You can apply animation to each view during the scroll: 5 | ![LTInfiniteScrollView](https://raw.githubusercontent.com/ltebean/LTInfiniteScrollView/master/demo/demo.gif) 6 | 7 | ##### 2. The iOS 9 task switcher animation can be implemented in ten minutes with the support of this lib: 8 | ![LTInfiniteScrollView](https://raw.githubusercontent.com/ltebean/LTInfiniteScrollView/master/demo/task-switcher-demo.gif) 9 | 10 | 11 | ##### 3. The fancy menu can also be implemented easily: 12 | ![LTInfiniteScrollView](https://raw.githubusercontent.com/ltebean/LTInfiniteScrollView/master/demo/menu-demo.gif) 13 | 14 | You can find the full demo in the Objective-C version: https://github.com/ltebean/LTInfiniteScrollView. 15 | 16 | ## Installation 17 | ``` 18 | pod 'LTInfiniteScrollViewSwift' 19 | ``` 20 | 21 | Or just copy `LTInfiniteScrollView.swift` into your project. 22 | 23 | 24 | ## Usage 25 | 26 | You can create the scroll view by: 27 | ```swift 28 | scrollView = LTInfiniteScrollView(frame: CGRect(x: 0, y: 200, width: screenWidth, height: 300)) 29 | scrollView.dataSource = self 30 | scrollView.delegate = self 31 | scrollView.maxScrollDistance = 5 32 | scrollView.reloadData(initialIndex: 0) 33 | ``` 34 | 35 | Then implement `LTInfiniteScrollViewDataSource` protocol: 36 | ```swift 37 | public protocol LTInfiniteScrollViewDataSource: class { 38 | func viewAtIndex(index: Int, reusingView view: UIView?) -> UIView 39 | func numberOfViews() -> Int 40 | func numberOfVisibleViews() -> Int 41 | } 42 | ``` 43 | 44 | Sample code: 45 | ```swift 46 | func numberOfViews() -> Int { 47 | return 100 48 | } 49 | 50 | func numberOfVisibleViews() -> Int { 51 | return 5 52 | } 53 | 54 | func viewAtIndex(index: Int, reusingView view: UIView?) -> UIView { 55 | if let label = view as? UILabel { 56 | label.text = "\(index)" 57 | return label 58 | } 59 | else { 60 | let size = screenWidth / CGFloat(numberOfVisibleViews()) 61 | let label = UILabel(frame: CGRect(x: 0, y: 0, width: size, height: size)) 62 | label.textAlignment = .Center 63 | label.backgroundColor = UIColor.darkGrayColor() 64 | label.textColor = UIColor.whiteColor() 65 | label.layer.cornerRadius = size / 2 66 | label.layer.masksToBounds = true 67 | label.text = "\(index)" 68 | return label 69 | } 70 | } 71 | ``` 72 | 73 | 74 | If you want to apply any animation during scrolling, implement `LTInfiniteScrollViewDelegate` protocol: 75 | ```swift 76 | public protocol LTInfiniteScrollViewDelegate: class { 77 | func updateView(view: UIView, withProgress progress: CGFloat, scrollDirection direction: ScrollDirection) 78 | } 79 | 80 | ``` 81 | The value of progress dependends on the position of that view, if there are 5 visible views, the value will be ranged from -2 to 2: 82 | ``` 83 | | | 84 | |-2 -1 0 1 2| 85 | | | 86 | ``` 87 | 88 | You can clone the project and investigate the example for details. 89 | -------------------------------------------------------------------------------- /LTInfiniteScrollView-Swift/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // LTInfiniteScrollView-Swift 4 | // 5 | // Created by ltebean on 16/1/8. 6 | // Copyright © 2016年 io. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | let screenWidth = UIScreen.main.bounds.size.width 14 | 15 | @IBOutlet weak var scrollView: LTInfiniteScrollView! 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | // Do any additional setup after loading the view, typically from a nib. 20 | scrollView.dataSource = self 21 | scrollView.delegate = self 22 | scrollView.maxScrollDistance = 5 23 | 24 | let size = screenWidth / CGFloat(numberOfVisibleViews()) 25 | 26 | scrollView.contentInset.left = screenWidth / 2 - size / 2 27 | scrollView.contentInset.right = screenWidth / 2 - size / 2 28 | 29 | view.addSubview(scrollView) 30 | } 31 | 32 | override func viewWillAppear(_ animated: Bool) { 33 | super.viewWillAppear(animated) 34 | scrollView.reloadData(initialIndex: 0) 35 | 36 | 37 | } 38 | } 39 | 40 | extension ViewController: LTInfiniteScrollViewDataSource { 41 | 42 | func viewAtIndex(_ index: Int, reusingView view: UIView?) -> UIView { 43 | if let label = view as? UILabel { 44 | label.text = "\(index)" 45 | return label 46 | } 47 | else { 48 | let size = screenWidth / CGFloat(numberOfVisibleViews()) 49 | let label = UILabel(frame: CGRect(x: 0, y: 0, width: size, height: size)) 50 | label.textAlignment = .center 51 | label.backgroundColor = UIColor.darkGray 52 | label.textColor = UIColor.white 53 | label.layer.cornerRadius = size / 2 54 | label.layer.masksToBounds = true 55 | label.text = "\(index)" 56 | return label 57 | } 58 | } 59 | 60 | func numberOfViews() -> Int { 61 | return 10 62 | } 63 | 64 | func numberOfVisibleViews() -> Int { 65 | return 5 66 | } 67 | } 68 | 69 | extension ViewController: LTInfiniteScrollViewDelegate { 70 | 71 | func updateView(_ view: UIView, withProgress progress: CGFloat, scrollDirection direction: LTInfiniteScrollView.ScrollDirection) { 72 | let size = screenWidth / CGFloat(numberOfVisibleViews()) 73 | 74 | var transform = CGAffineTransform.identity 75 | // scale 76 | let scale = (1.4 - 0.3 * (fabs(progress))) 77 | transform = transform.scaledBy(x: scale, y: scale) 78 | 79 | // translate 80 | var translate = size / 4 * progress 81 | if progress > 1 { 82 | translate = size / 4 83 | } 84 | else if progress < -1 { 85 | translate = -size / 4 86 | } 87 | transform = transform.translatedBy(x: translate, y: 0) 88 | 89 | view.transform = transform 90 | } 91 | 92 | func scrollViewDidScrollToIndex(_ scrollView: LTInfiniteScrollView, index: Int) { 93 | print("scroll to index: \(index)") 94 | } 95 | } 96 | 97 | -------------------------------------------------------------------------------- /LTInfiniteScrollView-Swift/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /LTInfiniteScrollView-Swift/Sources/LTInfiniteScrollView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LTInfiniteScrollView.swift 3 | // LTInfiniteScrollView-Swift 4 | // 5 | // Created by ltebean on 16/1/8. 6 | // Copyright © 2016年 io. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol LTInfiniteScrollViewDelegate: class { 12 | func updateView(_ view: UIView, withProgress progress: CGFloat, scrollDirection direction: LTInfiniteScrollView.ScrollDirection) 13 | func scrollViewDidScrollToIndex(_ scrollView: LTInfiniteScrollView, index: Int) 14 | } 15 | 16 | public protocol LTInfiniteScrollViewDataSource: class { 17 | func viewAtIndex(_ index: Int, reusingView view: UIView?) -> UIView 18 | func numberOfViews() -> Int 19 | func numberOfVisibleViews() -> Int 20 | } 21 | 22 | 23 | open class LTInfiniteScrollView: UIView { 24 | 25 | public enum ScrollDirection { 26 | case previous 27 | case next 28 | } 29 | 30 | override public init(frame: CGRect) { 31 | super.init(frame: frame) 32 | self.setup() 33 | } 34 | 35 | required public init?(coder aDecoder: NSCoder) { 36 | super.init(coder: aDecoder) 37 | self.setup() 38 | } 39 | 40 | open var pagingEnabled = false { 41 | didSet { 42 | scrollView.isPagingEnabled = pagingEnabled 43 | } 44 | } 45 | 46 | open var bounces = false { 47 | didSet { 48 | scrollView.bounces = bounces 49 | } 50 | } 51 | 52 | open var contentInset = UIEdgeInsets.zero { 53 | didSet { 54 | scrollView.contentInset = contentInset; 55 | } 56 | } 57 | 58 | open var scrollEnabled = false { 59 | didSet { 60 | scrollView.isScrollEnabled = scrollEnabled 61 | } 62 | } 63 | 64 | open var maxScrollDistance: Int? 65 | 66 | open fileprivate(set) var currentIndex = 0 67 | 68 | fileprivate var scrollView: UIScrollView! 69 | fileprivate var viewSize: CGSize! 70 | fileprivate var visibleViewCount = 0 71 | fileprivate var totalViewCount = 0 72 | fileprivate var preContentOffsetX: CGFloat = 0 73 | fileprivate var totalWidth: CGFloat = 0 74 | fileprivate var scrollDirection: ScrollDirection = .next 75 | fileprivate var views: [Int: UIView] = [:] 76 | 77 | open weak var delegate: LTInfiniteScrollViewDelegate? 78 | open var dataSource: LTInfiniteScrollViewDataSource! 79 | 80 | // MARK: public func 81 | open func reloadData(initialIndex: Int=0) { 82 | for view in scrollView.subviews { 83 | view.removeFromSuperview() 84 | } 85 | visibleViewCount = dataSource.numberOfVisibleViews() 86 | totalViewCount = dataSource.numberOfViews() 87 | updateContentSize() 88 | views = [:] 89 | currentIndex = initialIndex 90 | scrollView.contentOffset = contentOffsetForIndex(currentIndex) 91 | reArrangeViews() 92 | updateProgress() 93 | } 94 | 95 | open func scrollToIndex(_ index: Int, animated: Bool) { 96 | if index < currentIndex { 97 | scrollDirection = .previous 98 | } 99 | else { 100 | scrollDirection = .next 101 | } 102 | scrollView.setContentOffset(contentOffsetForIndex(index), animated: animated) 103 | } 104 | 105 | open func viewAtIndex(_ index: Int) -> UIView? { 106 | return views[index] 107 | } 108 | 109 | open func allViews() -> [UIView] { 110 | return [UIView](views.values) 111 | } 112 | 113 | open override func layoutSubviews() { 114 | let index = currentIndex; 115 | super.layoutSubviews() 116 | scrollView.frame = bounds 117 | updateContentSize() 118 | for (index, view) in views { 119 | view.center = self.centerForViewAtIndex(index) 120 | } 121 | scrollToIndex(index, animated: false) 122 | updateProgress() 123 | } 124 | 125 | // MARK: private func 126 | fileprivate func setup() { 127 | scrollView = UIScrollView(frame: CGRect(x: 0, y: 0, width: self.bounds.width, height: self.bounds.height)) 128 | scrollView.autoresizingMask = [UIViewAutoresizing.flexibleWidth, UIViewAutoresizing.flexibleHeight] 129 | scrollView.showsHorizontalScrollIndicator = false 130 | scrollView.delegate = self 131 | scrollView.clipsToBounds = false 132 | addSubview(self.scrollView) 133 | } 134 | 135 | 136 | fileprivate func updateContentSize() { 137 | let viewWidth = bounds.width / CGFloat(visibleViewCount) 138 | let viewHeight: CGFloat = bounds.height 139 | viewSize = CGSize(width: viewWidth, height: viewHeight) 140 | totalWidth = viewWidth * CGFloat(totalViewCount) 141 | scrollView.contentSize = CGSize(width: self.totalWidth, height: self.bounds.height) 142 | } 143 | 144 | fileprivate func reArrangeViews() { 145 | var indexesNeeded = Set() 146 | let begin = currentIndex - Int(ceil(Double(visibleViewCount) / 2.0)) 147 | let end = currentIndex + Int(ceil(Double(visibleViewCount) / 2.0)) 148 | for i in begin...end { 149 | if i < 0 { 150 | let index = end - i 151 | if index < totalViewCount { 152 | indexesNeeded.insert(index) 153 | } 154 | } 155 | else if i >= totalViewCount { 156 | let index = begin - i 157 | if index >= 0 { 158 | indexesNeeded.insert(index) 159 | } 160 | } 161 | else { 162 | indexesNeeded.insert(i) 163 | } 164 | } 165 | for indexNeeded in indexesNeeded { 166 | var view = views[indexNeeded] 167 | if view != nil { 168 | continue 169 | } 170 | let currentIndexes = [Int](views.keys) 171 | for index in currentIndexes { 172 | if !indexesNeeded.contains(index) { 173 | view = views[index] 174 | views.removeValue(forKey: index) 175 | break 176 | } 177 | } 178 | let viewNeeded = dataSource.viewAtIndex(indexNeeded, reusingView: view) 179 | viewNeeded.removeFromSuperview() 180 | viewNeeded.tag = indexNeeded 181 | viewNeeded.center = self.centerForViewAtIndex(indexNeeded) 182 | views[indexNeeded] = viewNeeded 183 | scrollView.addSubview(viewNeeded) 184 | } 185 | } 186 | 187 | fileprivate func updateProgress() { 188 | guard let delegate = delegate else { 189 | return 190 | } 191 | let currentCenterX = currentCenter().x 192 | for view in allViews() { 193 | let progress = (view.center.x - currentCenterX) / bounds.width * CGFloat(visibleViewCount) 194 | delegate.updateView(view, withProgress: progress, scrollDirection: scrollDirection) 195 | } 196 | } 197 | 198 | 199 | 200 | // MARK: helper 201 | fileprivate func needsCenterPage() -> Bool { 202 | let offsetX = scrollView.contentOffset.x 203 | if offsetX < -scrollView.contentInset.left || offsetX > scrollView.contentSize.width - viewSize.width { 204 | return false 205 | } 206 | else { 207 | return true 208 | } 209 | } 210 | 211 | fileprivate func currentCenter() -> CGPoint { 212 | let x = scrollView.contentOffset.x + bounds.width / 2.0 213 | let y = scrollView.contentOffset.y 214 | return CGPoint(x: x, y: y) 215 | } 216 | 217 | fileprivate func contentOffsetForIndex(_ index: Int) -> CGPoint { 218 | let centerX = centerForViewAtIndex(index).x 219 | var x: CGFloat = centerX - self.bounds.width / 2.0 220 | x = max(-scrollView.contentInset.left, x) 221 | x = min(x, scrollView.contentSize.width) 222 | return CGPoint(x: x, y: 0) 223 | } 224 | 225 | fileprivate func centerForViewAtIndex(_ index: Int) -> CGPoint { 226 | let y = bounds.midY 227 | let x = CGFloat(index) * viewSize.width + viewSize.width / 2 228 | return CGPoint(x: x, y: y) 229 | } 230 | 231 | fileprivate func didScrollToIndex(_ index : Int) { 232 | delegate?.scrollViewDidScrollToIndex(self, index: index) 233 | } 234 | } 235 | 236 | 237 | extension LTInfiniteScrollView: UIScrollViewDelegate { 238 | 239 | public func scrollViewDidScroll(_ scrollView: UIScrollView) { 240 | if viewSize == nil { 241 | return 242 | } 243 | let currentCenterX = currentCenter().x 244 | let offsetX = scrollView.contentOffset.x 245 | currentIndex = Int(round((currentCenterX - viewSize.width / 2) / viewSize.width)) 246 | if offsetX > preContentOffsetX { 247 | scrollDirection = .next 248 | } 249 | else { 250 | scrollDirection = .previous 251 | } 252 | preContentOffsetX = offsetX 253 | reArrangeViews() 254 | updateProgress() 255 | } 256 | 257 | public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { 258 | if !pagingEnabled && !decelerate && needsCenterPage() { 259 | let offsetX = scrollView.contentOffset.x 260 | if offsetX < 0 || offsetX > scrollView.contentSize.width { 261 | return 262 | } 263 | scrollView.setContentOffset(contentOffsetForIndex(currentIndex), animated: true) 264 | didScrollToIndex(currentIndex) 265 | } 266 | } 267 | 268 | public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { 269 | if !pagingEnabled && needsCenterPage() { 270 | scrollView.setContentOffset(contentOffsetForIndex(currentIndex), animated: true) 271 | } 272 | didScrollToIndex(currentIndex) 273 | } 274 | 275 | public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { 276 | 277 | guard let maxScrollDistance = maxScrollDistance , maxScrollDistance > 0 else { 278 | return 279 | } 280 | guard needsCenterPage() else { 281 | return 282 | } 283 | let targetX = targetContentOffset.pointee.x 284 | let currentX = contentOffsetForIndex(currentIndex).x 285 | if fabs(targetX - currentX) <= viewSize.width / 2 { 286 | targetContentOffset.pointee.x = contentOffsetForIndex(currentIndex).x 287 | } 288 | else { 289 | let distance = maxScrollDistance - 1 290 | var targetIndex = scrollDirection == .next ? currentIndex + distance : currentIndex - distance 291 | targetIndex = max(0, targetIndex) 292 | targetContentOffset.pointee.x = contentOffsetForIndex(targetIndex).x 293 | } 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /LTInfiniteScrollView-Swift.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | FA6635951C50BF8A00A12676 /* Lauch.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FA6635941C50BF8A00A12676 /* Lauch.storyboard */; }; 11 | FAA51EA41C3F7E9D00CD7F5A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAA51EA31C3F7E9D00CD7F5A /* AppDelegate.swift */; }; 12 | FAA51EA61C3F7E9D00CD7F5A /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAA51EA51C3F7E9D00CD7F5A /* ViewController.swift */; }; 13 | FAA51EA91C3F7E9D00CD7F5A /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FAA51EA71C3F7E9D00CD7F5A /* Main.storyboard */; }; 14 | FAA51EAB1C3F7E9D00CD7F5A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FAA51EAA1C3F7E9D00CD7F5A /* Assets.xcassets */; }; 15 | FAA51EB71C3F7EE400CD7F5A /* LTInfiniteScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAA51EB61C3F7EE400CD7F5A /* LTInfiniteScrollView.swift */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXFileReference section */ 19 | FA6635941C50BF8A00A12676 /* Lauch.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Lauch.storyboard; sourceTree = ""; }; 20 | FAA51EA01C3F7E9D00CD7F5A /* LTInfiniteScrollView-Swift.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "LTInfiniteScrollView-Swift.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 21 | FAA51EA31C3F7E9D00CD7F5A /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 22 | FAA51EA51C3F7E9D00CD7F5A /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 23 | FAA51EA81C3F7E9D00CD7F5A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 24 | FAA51EAA1C3F7E9D00CD7F5A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 25 | FAA51EAF1C3F7E9D00CD7F5A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 26 | FAA51EB61C3F7EE400CD7F5A /* LTInfiniteScrollView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LTInfiniteScrollView.swift; sourceTree = ""; }; 27 | /* End PBXFileReference section */ 28 | 29 | /* Begin PBXFrameworksBuildPhase section */ 30 | FAA51E9D1C3F7E9D00CD7F5A /* Frameworks */ = { 31 | isa = PBXFrameworksBuildPhase; 32 | buildActionMask = 2147483647; 33 | files = ( 34 | ); 35 | runOnlyForDeploymentPostprocessing = 0; 36 | }; 37 | /* End PBXFrameworksBuildPhase section */ 38 | 39 | /* Begin PBXGroup section */ 40 | FAA51E971C3F7E9D00CD7F5A = { 41 | isa = PBXGroup; 42 | children = ( 43 | FAA51EA21C3F7E9D00CD7F5A /* LTInfiniteScrollView-Swift */, 44 | FAA51EA11C3F7E9D00CD7F5A /* Products */, 45 | ); 46 | sourceTree = ""; 47 | }; 48 | FAA51EA11C3F7E9D00CD7F5A /* Products */ = { 49 | isa = PBXGroup; 50 | children = ( 51 | FAA51EA01C3F7E9D00CD7F5A /* LTInfiniteScrollView-Swift.app */, 52 | ); 53 | name = Products; 54 | sourceTree = ""; 55 | }; 56 | FAA51EA21C3F7E9D00CD7F5A /* LTInfiniteScrollView-Swift */ = { 57 | isa = PBXGroup; 58 | children = ( 59 | FAA51EB51C3F7EAA00CD7F5A /* Sources */, 60 | FAA51EA31C3F7E9D00CD7F5A /* AppDelegate.swift */, 61 | FAA51EA51C3F7E9D00CD7F5A /* ViewController.swift */, 62 | FAA51EA71C3F7E9D00CD7F5A /* Main.storyboard */, 63 | FA6635941C50BF8A00A12676 /* Lauch.storyboard */, 64 | FAA51EAA1C3F7E9D00CD7F5A /* Assets.xcassets */, 65 | FAA51EAF1C3F7E9D00CD7F5A /* Info.plist */, 66 | ); 67 | path = "LTInfiniteScrollView-Swift"; 68 | sourceTree = ""; 69 | }; 70 | FAA51EB51C3F7EAA00CD7F5A /* Sources */ = { 71 | isa = PBXGroup; 72 | children = ( 73 | FAA51EB61C3F7EE400CD7F5A /* LTInfiniteScrollView.swift */, 74 | ); 75 | path = Sources; 76 | sourceTree = ""; 77 | }; 78 | /* End PBXGroup section */ 79 | 80 | /* Begin PBXNativeTarget section */ 81 | FAA51E9F1C3F7E9D00CD7F5A /* LTInfiniteScrollView-Swift */ = { 82 | isa = PBXNativeTarget; 83 | buildConfigurationList = FAA51EB21C3F7E9D00CD7F5A /* Build configuration list for PBXNativeTarget "LTInfiniteScrollView-Swift" */; 84 | buildPhases = ( 85 | FAA51E9C1C3F7E9D00CD7F5A /* Sources */, 86 | FAA51E9D1C3F7E9D00CD7F5A /* Frameworks */, 87 | FAA51E9E1C3F7E9D00CD7F5A /* Resources */, 88 | ); 89 | buildRules = ( 90 | ); 91 | dependencies = ( 92 | ); 93 | name = "LTInfiniteScrollView-Swift"; 94 | productName = "LTInfiniteScrollView-Swift"; 95 | productReference = FAA51EA01C3F7E9D00CD7F5A /* LTInfiniteScrollView-Swift.app */; 96 | productType = "com.apple.product-type.application"; 97 | }; 98 | /* End PBXNativeTarget section */ 99 | 100 | /* Begin PBXProject section */ 101 | FAA51E981C3F7E9D00CD7F5A /* Project object */ = { 102 | isa = PBXProject; 103 | attributes = { 104 | LastSwiftUpdateCheck = 0710; 105 | LastUpgradeCheck = 0800; 106 | ORGANIZATIONNAME = io; 107 | TargetAttributes = { 108 | FAA51E9F1C3F7E9D00CD7F5A = { 109 | CreatedOnToolsVersion = 7.1; 110 | LastSwiftMigration = 0800; 111 | }; 112 | }; 113 | }; 114 | buildConfigurationList = FAA51E9B1C3F7E9D00CD7F5A /* Build configuration list for PBXProject "LTInfiniteScrollView-Swift" */; 115 | compatibilityVersion = "Xcode 3.2"; 116 | developmentRegion = English; 117 | hasScannedForEncodings = 0; 118 | knownRegions = ( 119 | en, 120 | Base, 121 | ); 122 | mainGroup = FAA51E971C3F7E9D00CD7F5A; 123 | productRefGroup = FAA51EA11C3F7E9D00CD7F5A /* Products */; 124 | projectDirPath = ""; 125 | projectRoot = ""; 126 | targets = ( 127 | FAA51E9F1C3F7E9D00CD7F5A /* LTInfiniteScrollView-Swift */, 128 | ); 129 | }; 130 | /* End PBXProject section */ 131 | 132 | /* Begin PBXResourcesBuildPhase section */ 133 | FAA51E9E1C3F7E9D00CD7F5A /* Resources */ = { 134 | isa = PBXResourcesBuildPhase; 135 | buildActionMask = 2147483647; 136 | files = ( 137 | FAA51EAB1C3F7E9D00CD7F5A /* Assets.xcassets in Resources */, 138 | FA6635951C50BF8A00A12676 /* Lauch.storyboard in Resources */, 139 | FAA51EA91C3F7E9D00CD7F5A /* Main.storyboard in Resources */, 140 | ); 141 | runOnlyForDeploymentPostprocessing = 0; 142 | }; 143 | /* End PBXResourcesBuildPhase section */ 144 | 145 | /* Begin PBXSourcesBuildPhase section */ 146 | FAA51E9C1C3F7E9D00CD7F5A /* Sources */ = { 147 | isa = PBXSourcesBuildPhase; 148 | buildActionMask = 2147483647; 149 | files = ( 150 | FAA51EB71C3F7EE400CD7F5A /* LTInfiniteScrollView.swift in Sources */, 151 | FAA51EA61C3F7E9D00CD7F5A /* ViewController.swift in Sources */, 152 | FAA51EA41C3F7E9D00CD7F5A /* AppDelegate.swift in Sources */, 153 | ); 154 | runOnlyForDeploymentPostprocessing = 0; 155 | }; 156 | /* End PBXSourcesBuildPhase section */ 157 | 158 | /* Begin PBXVariantGroup section */ 159 | FAA51EA71C3F7E9D00CD7F5A /* Main.storyboard */ = { 160 | isa = PBXVariantGroup; 161 | children = ( 162 | FAA51EA81C3F7E9D00CD7F5A /* Base */, 163 | ); 164 | name = Main.storyboard; 165 | sourceTree = ""; 166 | }; 167 | /* End PBXVariantGroup section */ 168 | 169 | /* Begin XCBuildConfiguration section */ 170 | FAA51EB01C3F7E9D00CD7F5A /* Debug */ = { 171 | isa = XCBuildConfiguration; 172 | buildSettings = { 173 | ALWAYS_SEARCH_USER_PATHS = NO; 174 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 175 | CLANG_CXX_LIBRARY = "libc++"; 176 | CLANG_ENABLE_MODULES = YES; 177 | CLANG_ENABLE_OBJC_ARC = YES; 178 | CLANG_WARN_BOOL_CONVERSION = YES; 179 | CLANG_WARN_CONSTANT_CONVERSION = YES; 180 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 181 | CLANG_WARN_EMPTY_BODY = YES; 182 | CLANG_WARN_ENUM_CONVERSION = YES; 183 | CLANG_WARN_INFINITE_RECURSION = YES; 184 | CLANG_WARN_INT_CONVERSION = YES; 185 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 186 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 187 | CLANG_WARN_UNREACHABLE_CODE = YES; 188 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 189 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 190 | COPY_PHASE_STRIP = NO; 191 | DEBUG_INFORMATION_FORMAT = dwarf; 192 | ENABLE_STRICT_OBJC_MSGSEND = YES; 193 | ENABLE_TESTABILITY = YES; 194 | GCC_C_LANGUAGE_STANDARD = gnu99; 195 | GCC_DYNAMIC_NO_PIC = NO; 196 | GCC_NO_COMMON_BLOCKS = YES; 197 | GCC_OPTIMIZATION_LEVEL = 0; 198 | GCC_PREPROCESSOR_DEFINITIONS = ( 199 | "DEBUG=1", 200 | "$(inherited)", 201 | ); 202 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 203 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 204 | GCC_WARN_UNDECLARED_SELECTOR = YES; 205 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 206 | GCC_WARN_UNUSED_FUNCTION = YES; 207 | GCC_WARN_UNUSED_VARIABLE = YES; 208 | IPHONEOS_DEPLOYMENT_TARGET = 9.1; 209 | MTL_ENABLE_DEBUG_INFO = YES; 210 | ONLY_ACTIVE_ARCH = YES; 211 | SDKROOT = iphoneos; 212 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 213 | }; 214 | name = Debug; 215 | }; 216 | FAA51EB11C3F7E9D00CD7F5A /* Release */ = { 217 | isa = XCBuildConfiguration; 218 | buildSettings = { 219 | ALWAYS_SEARCH_USER_PATHS = NO; 220 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 221 | CLANG_CXX_LIBRARY = "libc++"; 222 | CLANG_ENABLE_MODULES = YES; 223 | CLANG_ENABLE_OBJC_ARC = YES; 224 | CLANG_WARN_BOOL_CONVERSION = YES; 225 | CLANG_WARN_CONSTANT_CONVERSION = YES; 226 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 227 | CLANG_WARN_EMPTY_BODY = YES; 228 | CLANG_WARN_ENUM_CONVERSION = YES; 229 | CLANG_WARN_INFINITE_RECURSION = YES; 230 | CLANG_WARN_INT_CONVERSION = YES; 231 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 232 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 233 | CLANG_WARN_UNREACHABLE_CODE = YES; 234 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 235 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 236 | COPY_PHASE_STRIP = NO; 237 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 238 | ENABLE_NS_ASSERTIONS = NO; 239 | ENABLE_STRICT_OBJC_MSGSEND = YES; 240 | GCC_C_LANGUAGE_STANDARD = gnu99; 241 | GCC_NO_COMMON_BLOCKS = YES; 242 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 243 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 244 | GCC_WARN_UNDECLARED_SELECTOR = YES; 245 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 246 | GCC_WARN_UNUSED_FUNCTION = YES; 247 | GCC_WARN_UNUSED_VARIABLE = YES; 248 | IPHONEOS_DEPLOYMENT_TARGET = 9.1; 249 | MTL_ENABLE_DEBUG_INFO = NO; 250 | SDKROOT = iphoneos; 251 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 252 | VALIDATE_PRODUCT = YES; 253 | }; 254 | name = Release; 255 | }; 256 | FAA51EB31C3F7E9D00CD7F5A /* Debug */ = { 257 | isa = XCBuildConfiguration; 258 | buildSettings = { 259 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 260 | INFOPLIST_FILE = "LTInfiniteScrollView-Swift/Info.plist"; 261 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 262 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 263 | PRODUCT_BUNDLE_IDENTIFIER = "ltebean.LTInfiniteScrollView-Swift"; 264 | PRODUCT_NAME = "$(TARGET_NAME)"; 265 | SWIFT_VERSION = 3.0; 266 | }; 267 | name = Debug; 268 | }; 269 | FAA51EB41C3F7E9D00CD7F5A /* Release */ = { 270 | isa = XCBuildConfiguration; 271 | buildSettings = { 272 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 273 | INFOPLIST_FILE = "LTInfiniteScrollView-Swift/Info.plist"; 274 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 275 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 276 | PRODUCT_BUNDLE_IDENTIFIER = "ltebean.LTInfiniteScrollView-Swift"; 277 | PRODUCT_NAME = "$(TARGET_NAME)"; 278 | SWIFT_VERSION = 3.0; 279 | }; 280 | name = Release; 281 | }; 282 | /* End XCBuildConfiguration section */ 283 | 284 | /* Begin XCConfigurationList section */ 285 | FAA51E9B1C3F7E9D00CD7F5A /* Build configuration list for PBXProject "LTInfiniteScrollView-Swift" */ = { 286 | isa = XCConfigurationList; 287 | buildConfigurations = ( 288 | FAA51EB01C3F7E9D00CD7F5A /* Debug */, 289 | FAA51EB11C3F7E9D00CD7F5A /* Release */, 290 | ); 291 | defaultConfigurationIsVisible = 0; 292 | defaultConfigurationName = Release; 293 | }; 294 | FAA51EB21C3F7E9D00CD7F5A /* Build configuration list for PBXNativeTarget "LTInfiniteScrollView-Swift" */ = { 295 | isa = XCConfigurationList; 296 | buildConfigurations = ( 297 | FAA51EB31C3F7E9D00CD7F5A /* Debug */, 298 | FAA51EB41C3F7E9D00CD7F5A /* Release */, 299 | ); 300 | defaultConfigurationIsVisible = 0; 301 | defaultConfigurationName = Release; 302 | }; 303 | /* End XCConfigurationList section */ 304 | }; 305 | rootObject = FAA51E981C3F7E9D00CD7F5A /* Project object */; 306 | } 307 | --------------------------------------------------------------------------------