├── .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 | 
2 |
3 | ## Demo
4 | ##### 1. You can apply animation to each view during the scroll:
5 | 
6 |
7 | ##### 2. The iOS 9 task switcher animation can be implemented in ten minutes with the support of this lib:
8 | 
9 |
10 |
11 | ##### 3. The fancy menu can also be implemented easily:
12 | 
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 |
--------------------------------------------------------------------------------