├── .swift-version
├── DGElasticPullToRefreshPreview1.gif
├── DGElasticPullToRefreshPreview2.gif
├── DGElasticPullToRefreshExample.xcodeproj
├── xcuserdata
│ ├── danil.gontovik.xcuserdatad
│ │ ├── xcdebugger
│ │ │ └── Breakpoints_v2.xcbkptlist
│ │ └── xcschemes
│ │ │ └── DGElasticPullToRefreshExample.xcscheme
│ ├── haawa799.xcuserdatad
│ │ └── xcschemes
│ │ │ ├── xcschememanagement.plist
│ │ │ └── DGElasticPullToRefreshExample.xcscheme
│ └── gontovnik.xcuserdatad
│ │ └── xcschemes
│ │ ├── xcschememanagement.plist
│ │ └── DGElasticPullToRefreshExample.xcscheme
├── project.xcworkspace
│ ├── xcuserdata
│ │ ├── haawa799.xcuserdatad
│ │ │ └── UserInterfaceState.xcuserstate
│ │ └── gontovnik.xcuserdatad
│ │ │ └── UserInterfaceState.xcuserstate
│ └── contents.xcworkspacedata
└── project.pbxproj
├── DGElasticPullToRefreshExample
├── NavigationController.swift
├── AppDelegate.swift
├── Assets.xcassets
│ └── AppIcon.appiconset
│ │ └── Contents.json
├── Info.plist
├── Base.lproj
│ └── LaunchScreen.storyboard
└── ViewController.swift
├── DGElasticPullToRefresh.podspec
├── LICENSE
├── DGElasticPullToRefresh
├── DGElasticPullToRefreshConstants.swift
├── DGElasticPullToRefreshLoadingView.swift
├── DGElasticPullToRefreshLoadingViewCircle.swift
├── DGElasticPullToRefreshExtensions.swift
└── DGElasticPullToRefreshView.swift
└── README.md
/.swift-version:
--------------------------------------------------------------------------------
1 | 3.0
2 |
--------------------------------------------------------------------------------
/DGElasticPullToRefreshPreview1.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gontovnik/DGElasticPullToRefresh/HEAD/DGElasticPullToRefreshPreview1.gif
--------------------------------------------------------------------------------
/DGElasticPullToRefreshPreview2.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gontovnik/DGElasticPullToRefresh/HEAD/DGElasticPullToRefreshPreview2.gif
--------------------------------------------------------------------------------
/DGElasticPullToRefreshExample.xcodeproj/xcuserdata/danil.gontovik.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
--------------------------------------------------------------------------------
/DGElasticPullToRefreshExample.xcodeproj/project.xcworkspace/xcuserdata/haawa799.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gontovnik/DGElasticPullToRefresh/HEAD/DGElasticPullToRefreshExample.xcodeproj/project.xcworkspace/xcuserdata/haawa799.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/DGElasticPullToRefreshExample.xcodeproj/project.xcworkspace/xcuserdata/gontovnik.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gontovnik/DGElasticPullToRefresh/HEAD/DGElasticPullToRefreshExample.xcodeproj/project.xcworkspace/xcuserdata/gontovnik.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/DGElasticPullToRefreshExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/DGElasticPullToRefreshExample/NavigationController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NavigationController.swift
3 | // DGElasticPullToRefreshExample
4 | //
5 | // Created by Danil Gontovnik on 10/2/15.
6 | // Copyright © 2015 Danil Gontovnik. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class NavigationController: UINavigationController {
12 |
13 | // MARK: -
14 |
15 | override var preferredStatusBarStyle: UIStatusBarStyle {
16 | return .lightContent
17 | }
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/DGElasticPullToRefreshExample.xcodeproj/xcuserdata/haawa799.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | DGElasticPullToRefreshExample.xcscheme
8 |
9 | orderHint
10 | 0
11 |
12 |
13 | SuppressBuildableAutocreation
14 |
15 | 05CD14641BBE8FEA00AF4030
16 |
17 | primary
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/DGElasticPullToRefreshExample.xcodeproj/xcuserdata/gontovnik.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | DGElasticPullToRefreshExample.xcscheme
8 |
9 | orderHint
10 | 0
11 |
12 |
13 | SuppressBuildableAutocreation
14 |
15 | 05CD14641BBE8FEA00AF4030
16 |
17 | primary
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/DGElasticPullToRefresh.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |spec|
2 | spec.name = "DGElasticPullToRefresh"
3 | spec.version = "1.1"
4 | spec.authors = { "Danil Gontovnik" => "gontovnik.danil@gmail.com" }
5 | spec.homepage = "https://github.com/gontovnik/DGElasticPullToRefresh"
6 | spec.summary = "Elastic pull to refresh compontent developed in Swift"
7 | spec.source = { :git => "https://github.com/gontovnik/DGElasticPullToRefresh.git",
8 | :tag => '1.1' }
9 | spec.license = { :type => "MIT", :file => "LICENSE" }
10 | spec.platform = :ios, '8.0'
11 | spec.source_files = "DGElasticPullToRefresh/*.swift"
12 |
13 | spec.requires_arc = true
14 |
15 | spec.ios.deployment_target = '8.0'
16 | spec.ios.frameworks = ['UIKit', 'Foundation']
17 | end
18 |
--------------------------------------------------------------------------------
/DGElasticPullToRefreshExample/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // DGElasticPullToRefreshExample
4 | //
5 | // Created by Danil Gontovnik on 10/2/15.
6 | // Copyright © 2015 Danil Gontovnik. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @UIApplicationMain
12 | class AppDelegate: UIResponder, UIApplicationDelegate {
13 |
14 | var window: UIWindow?
15 |
16 | func applicationDidFinishLaunching(_ application: UIApplication) {
17 | let viewController = ViewController()
18 | let navigationController = NavigationController(rootViewController: viewController)
19 |
20 | window = UIWindow(frame: UIScreen.main.bounds)
21 | window!.rootViewController = navigationController
22 | window!.backgroundColor = .white
23 | window!.makeKeyAndVisible()
24 | }
25 |
26 | }
27 |
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Danil Gontovnik
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/DGElasticPullToRefreshExample/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 | "idiom" : "ipad",
35 | "size" : "29x29",
36 | "scale" : "1x"
37 | },
38 | {
39 | "idiom" : "ipad",
40 | "size" : "29x29",
41 | "scale" : "2x"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "size" : "40x40",
46 | "scale" : "1x"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "size" : "40x40",
51 | "scale" : "2x"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "size" : "76x76",
56 | "scale" : "1x"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "size" : "76x76",
61 | "scale" : "2x"
62 | }
63 | ],
64 | "info" : {
65 | "version" : 1,
66 | "author" : "xcode"
67 | }
68 | }
--------------------------------------------------------------------------------
/DGElasticPullToRefreshExample/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.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 | LSRequiresIPhoneOS
24 |
25 | UILaunchStoryboardName
26 | LaunchScreen
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 | UIViewControllerBasedStatusBarAppearance
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/DGElasticPullToRefreshExample/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 |
27 |
28 |
--------------------------------------------------------------------------------
/DGElasticPullToRefresh/DGElasticPullToRefreshConstants.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | The MIT License (MIT)
4 |
5 | Copyright (c) 2015 Danil Gontovnik
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy
8 | of this software and associated documentation files (the "Software"), to deal
9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | SOFTWARE.
24 |
25 | */
26 |
27 | import CoreGraphics
28 |
29 | public struct DGElasticPullToRefreshConstants {
30 |
31 | struct KeyPaths {
32 | static let ContentOffset = "contentOffset"
33 | static let ContentInset = "contentInset"
34 | static let Frame = "frame"
35 | static let PanGestureRecognizerState = "panGestureRecognizer.state"
36 | }
37 |
38 | public static var WaveMaxHeight: CGFloat = 70.0
39 | public static var MinOffsetToPull: CGFloat = 95.0
40 | public static var LoadingContentInset: CGFloat = 50.0
41 | public static var LoadingViewSize: CGFloat = 30.0
42 |
43 | }
--------------------------------------------------------------------------------
/DGElasticPullToRefresh/DGElasticPullToRefreshLoadingView.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | The MIT License (MIT)
4 |
5 | Copyright (c) 2015 Danil Gontovnik
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy
8 | of this software and associated documentation files (the "Software"), to deal
9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | SOFTWARE.
24 |
25 | */
26 |
27 | import UIKit
28 |
29 | open class DGElasticPullToRefreshLoadingView: UIView {
30 |
31 | // MARK: -
32 | // MARK: Vars
33 |
34 | lazy var maskLayer: CAShapeLayer = {
35 | let maskLayer = CAShapeLayer()
36 | maskLayer.backgroundColor = UIColor.clear.cgColor
37 | maskLayer.fillColor = UIColor.black.cgColor
38 | maskLayer.actions = ["path" : NSNull(), "position" : NSNull(), "bounds" : NSNull()]
39 | self.layer.mask = maskLayer
40 | return maskLayer
41 | }()
42 |
43 | // MARK: -
44 | // MARK: Constructors
45 |
46 | public init() {
47 | super.init(frame: .zero)
48 | }
49 |
50 | public override init(frame: CGRect) {
51 | super.init(frame: .zero)
52 | }
53 |
54 | required public init?(coder aDecoder: NSCoder) {
55 | fatalError("init(coder:) has not been implemented")
56 | }
57 |
58 | // MARK: -
59 | // MARK: Methods
60 |
61 | open func setPullProgress(_ progress: CGFloat) {
62 |
63 | }
64 |
65 | open func startAnimating() {
66 |
67 | }
68 |
69 | open func stopLoading() {
70 |
71 | }
72 |
73 | }
74 |
--------------------------------------------------------------------------------
/DGElasticPullToRefreshExample/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // DGElasticPullToRefreshExample
4 | //
5 | // Created by Danil Gontovnik on 10/2/15.
6 | // Copyright © 2015 Danil Gontovnik. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class ViewController: UIViewController {
12 |
13 | // MARK: -
14 | // MARK: Vars
15 |
16 | fileprivate var tableView: UITableView!
17 |
18 | // MARK: -
19 |
20 | override func loadView() {
21 | super.loadView()
22 |
23 | navigationController?.navigationBar.isTranslucent = false
24 | navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default)
25 | navigationController?.navigationBar.shadowImage = UIImage()
26 | navigationController?.navigationBar.barTintColor = UIColor(red: 57/255.0, green: 67/255.0, blue: 89/255.0, alpha: 1.0)
27 |
28 | tableView = UITableView(frame: view.bounds, style: .plain)
29 | tableView.dataSource = self
30 | tableView.delegate = self
31 | tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
32 | tableView.separatorColor = UIColor(red: 230/255.0, green: 230/255.0, blue: 231/255.0, alpha: 1.0)
33 | tableView.backgroundColor = UIColor(red: 250/255.0, green: 250/255.0, blue: 251/255.0, alpha: 1.0)
34 | view.addSubview(tableView)
35 |
36 | let loadingView = DGElasticPullToRefreshLoadingViewCircle()
37 | loadingView.tintColor = UIColor(red: 78/255.0, green: 221/255.0, blue: 200/255.0, alpha: 1.0)
38 | tableView.dg_addPullToRefreshWithActionHandler({ [weak self] () -> Void in
39 | DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(Int64(1.5 * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC), execute: {
40 | self?.tableView.dg_stopLoading()
41 | })
42 | }, loadingView: loadingView)
43 | tableView.dg_setPullToRefreshFillColor(UIColor(red: 57/255.0, green: 67/255.0, blue: 89/255.0, alpha: 1.0))
44 | tableView.dg_setPullToRefreshBackgroundColor(tableView.backgroundColor!)
45 | }
46 |
47 | deinit {
48 | tableView.dg_removePullToRefresh()
49 | }
50 |
51 | }
52 |
53 | // MARK: -
54 | // MARK: UITableView Data Source
55 |
56 | extension ViewController: UITableViewDataSource {
57 |
58 | func numberOfSections(in tableView: UITableView) -> Int {
59 | return 1
60 | }
61 |
62 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
63 | return 30
64 | }
65 |
66 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
67 | let cellIdentifier = "cellIdentifier"
68 | var cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier)
69 |
70 | if cell == nil {
71 | cell = UITableViewCell(style: .default, reuseIdentifier: cellIdentifier)
72 | cell!.contentView.backgroundColor = UIColor(red: 250/255.0, green: 250/255.0, blue: 251/255.0, alpha: 1.0)
73 | }
74 |
75 | if let cell = cell {
76 | cell.textLabel?.text = "\((indexPath as NSIndexPath).row)"
77 | return cell
78 | }
79 |
80 | return UITableViewCell()
81 | }
82 |
83 | }
84 |
85 | // MARK: -
86 | // MARK: UITableView Delegate
87 |
88 | extension ViewController: UITableViewDelegate {
89 |
90 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
91 | tableView.deselectRow(at: indexPath, animated: true)
92 | }
93 |
94 | }
95 |
--------------------------------------------------------------------------------
/DGElasticPullToRefreshExample.xcodeproj/xcuserdata/haawa799.xcuserdatad/xcschemes/DGElasticPullToRefreshExample.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
39 |
40 |
41 |
42 |
43 |
44 |
54 |
56 |
62 |
63 |
64 |
65 |
66 |
67 |
73 |
75 |
81 |
82 |
83 |
84 |
86 |
87 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/DGElasticPullToRefreshExample.xcodeproj/xcuserdata/gontovnik.xcuserdatad/xcschemes/DGElasticPullToRefreshExample.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
39 |
40 |
41 |
42 |
43 |
44 |
54 |
56 |
62 |
63 |
64 |
65 |
66 |
67 |
73 |
75 |
81 |
82 |
83 |
84 |
86 |
87 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/DGElasticPullToRefreshExample.xcodeproj/xcuserdata/danil.gontovik.xcuserdatad/xcschemes/DGElasticPullToRefreshExample.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
39 |
40 |
41 |
42 |
43 |
44 |
54 |
56 |
62 |
63 |
64 |
65 |
66 |
67 |
73 |
75 |
81 |
82 |
83 |
84 |
86 |
87 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # DGElasticPullToRefresh
2 | Elastic pull to refresh compontent developed in Swift
3 |
4 | Inspired by this Dribbble post: [Pull Down to Refresh](https://dribbble.com/shots/2232385-Pull-Down-to-Refresh) by [Hoang Nguyen](https://dribbble.com/Hoanguyen)
5 |
6 | Tutorial on how this bounce effect was achieved can be found [here](https://medium.com/@gontovnik/elastic-view-animation-or-how-i-built-dgelasticpulltorefresh-269a3ba8636e#.9dioekqv6).
7 |
8 | 
9 | 
10 |
11 | ## Requirements
12 | * Xcode 7 or higher
13 | * iOS 8.0 or higher (may work on previous versions, just did not test it)
14 | * ARC
15 | * Swift 3.0
16 |
17 | ## Demo
18 |
19 | Open and run the DGElasticPullToRefreshExample project in Xcode to see DGElasticPullToRefresh in action.
20 |
21 | ## Installation
22 |
23 | ### CocoaPods
24 |
25 | ``` ruby
26 | pod 'DGElasticPullToRefresh'
27 | ```
28 |
29 | ### Manual
30 |
31 | Add DGElasticPullToRefresh folder into your project.
32 |
33 | ## Example usage
34 |
35 | ``` swift
36 | // Initialize tableView
37 | let loadingView = DGElasticPullToRefreshLoadingViewCircle()
38 | loadingView.tintColor = UIColor(red: 78/255.0, green: 221/255.0, blue: 200/255.0, alpha: 1.0)
39 | tableView.dg_addPullToRefreshWithActionHandler({ [weak self] () -> Void in
40 | // Add your logic here
41 | // Do not forget to call dg_stopLoading() at the end
42 | self?.tableView.dg_stopLoading()
43 | }, loadingView: loadingView)
44 | tableView.dg_setPullToRefreshFillColor(UIColor(red: 57/255.0, green: 67/255.0, blue: 89/255.0, alpha: 1.0))
45 | tableView.dg_setPullToRefreshBackgroundColor(tableView.backgroundColor!)
46 | ```
47 |
48 | Do not forget to remove pull to refresh on view controller deinit. It is a temporary solution.
49 |
50 | ``` swift
51 | deinit {
52 | tableView.dg_removePullToRefresh()
53 | }
54 | ```
55 |
56 | ### Description
57 |
58 | Add pull to refresh without loading view:
59 |
60 | ``` swift
61 | func dg_addPullToRefreshWithActionHandler(_ actionHandler: @escaping () -> Void)
62 | ```
63 |
64 | Add pull to refresh with loading view:
65 |
66 | ``` swift
67 | func dg_addPullToRefreshWithActionHandler(_ actionHandler: @escaping () -> Void, loadingView: DGElasticPullToRefreshLoadingView?)
68 | ```
69 |
70 | You can use built-in *DGElasticPullToRefreshLoadingViewCircle* or create your own by subclassing **DGElasticPullToRefreshLoadingView** and implementing these methods:
71 |
72 | ``` swift
73 | func setPullProgress(_ progress: CGFloat)
74 | func startAnimating()
75 | func stopLoading()
76 | ```
77 |
78 | Remove pull to refresh:
79 |
80 | ``` swift
81 | func dg_removePullToRefresh()
82 | ```
83 |
84 | Set auto start loading:
85 |
86 | ``` swift
87 | func dg_startLoading()
88 | ```
89 |
90 | Change pull to refresh background color:
91 |
92 | ``` swift
93 | func dg_setPullToRefreshBackgroundColor(_ color: UIColor)
94 | ```
95 |
96 | Change pull to refresh fill color:
97 |
98 | ``` swift
99 | func dg_setPullToRefreshFillColor(_ color: UIColor)
100 | ```
101 |
102 | ## Contribution
103 |
104 | Please feel free to submit pull requests. Cannot wait to see your custom loading views for this pull to refresh.
105 |
106 | ## Contact
107 |
108 | Danil Gontovnik
109 |
110 | - https://github.com/gontovnik
111 | - https://twitter.com/gontovnik
112 | - http://gontovnik.com/
113 | - danil@gontovnik.com
114 |
115 | ## License
116 |
117 | The MIT License (MIT)
118 |
119 | Copyright (c) 2015 Danil Gontovnik
120 |
121 | Permission is hereby granted, free of charge, to any person obtaining a copy
122 | of this software and associated documentation files (the "Software"), to deal
123 | in the Software without restriction, including without limitation the rights
124 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
125 | copies of the Software, and to permit persons to whom the Software is
126 | furnished to do so, subject to the following conditions:
127 |
128 | The above copyright notice and this permission notice shall be included in all
129 | copies or substantial portions of the Software.
130 |
131 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
132 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
133 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
134 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
135 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
136 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
137 | SOFTWARE.
138 |
--------------------------------------------------------------------------------
/DGElasticPullToRefresh/DGElasticPullToRefreshLoadingViewCircle.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | The MIT License (MIT)
4 |
5 | Copyright (c) 2015 Danil Gontovnik
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy
8 | of this software and associated documentation files (the "Software"), to deal
9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | SOFTWARE.
24 |
25 | */
26 |
27 | import UIKit
28 |
29 | // MARK: -
30 | // MARK: (CGFloat) Extension
31 |
32 | public extension CGFloat {
33 |
34 | public func toRadians() -> CGFloat {
35 | return (self * CGFloat(M_PI)) / 180.0
36 | }
37 |
38 | public func toDegrees() -> CGFloat {
39 | return self * 180.0 / CGFloat(M_PI)
40 | }
41 |
42 | }
43 |
44 | // MARK: -
45 | // MARK: DGElasticPullToRefreshLoadingViewCircle
46 |
47 | open class DGElasticPullToRefreshLoadingViewCircle: DGElasticPullToRefreshLoadingView {
48 |
49 | // MARK: -
50 | // MARK: Vars
51 |
52 | fileprivate let kRotationAnimation = "kRotationAnimation"
53 |
54 | fileprivate let shapeLayer = CAShapeLayer()
55 | fileprivate lazy var identityTransform: CATransform3D = {
56 | var transform = CATransform3DIdentity
57 | transform.m34 = CGFloat(1.0 / -500.0)
58 | transform = CATransform3DRotate(transform, CGFloat(-90.0).toRadians(), 0.0, 0.0, 1.0)
59 | return transform
60 | }()
61 |
62 | // MARK: -
63 | // MARK: Constructors
64 |
65 | public override init() {
66 | super.init(frame: .zero)
67 |
68 | shapeLayer.lineWidth = 1.0
69 | shapeLayer.fillColor = UIColor.clear.cgColor
70 | shapeLayer.strokeColor = tintColor.cgColor
71 | shapeLayer.actions = ["strokeEnd" : NSNull(), "transform" : NSNull()]
72 | shapeLayer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
73 | layer.addSublayer(shapeLayer)
74 | }
75 |
76 | required public init?(coder aDecoder: NSCoder) {
77 | fatalError("init(coder:) has not been implemented")
78 | }
79 |
80 | // MARK: -
81 | // MARK: Methods
82 |
83 | override open func setPullProgress(_ progress: CGFloat) {
84 | super.setPullProgress(progress)
85 |
86 | shapeLayer.strokeEnd = min(0.9 * progress, 0.9)
87 |
88 | if progress > 1.0 {
89 | let degrees = ((progress - 1.0) * 200.0)
90 | shapeLayer.transform = CATransform3DRotate(identityTransform, degrees.toRadians(), 0.0, 0.0, 1.0)
91 | } else {
92 | shapeLayer.transform = identityTransform
93 | }
94 | }
95 |
96 | override open func startAnimating() {
97 | super.startAnimating()
98 |
99 | if shapeLayer.animation(forKey: kRotationAnimation) != nil { return }
100 |
101 | let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
102 | rotationAnimation.toValue = CGFloat(2 * M_PI) + currentDegree()
103 | rotationAnimation.duration = 1.0
104 | rotationAnimation.repeatCount = Float.infinity
105 | rotationAnimation.isRemovedOnCompletion = false
106 | rotationAnimation.fillMode = kCAFillModeForwards
107 | shapeLayer.add(rotationAnimation, forKey: kRotationAnimation)
108 | }
109 |
110 | override open func stopLoading() {
111 | super.stopLoading()
112 |
113 | shapeLayer.removeAnimation(forKey: kRotationAnimation)
114 | }
115 |
116 | fileprivate func currentDegree() -> CGFloat {
117 | return shapeLayer.value(forKeyPath: "transform.rotation.z") as! CGFloat
118 | }
119 |
120 | override open func tintColorDidChange() {
121 | super.tintColorDidChange()
122 |
123 | shapeLayer.strokeColor = tintColor.cgColor
124 | }
125 |
126 | // MARK: -
127 | // MARK: Layout
128 |
129 | override open func layoutSubviews() {
130 | super.layoutSubviews()
131 |
132 | shapeLayer.frame = bounds
133 |
134 | let inset = shapeLayer.lineWidth / 2.0
135 | shapeLayer.path = UIBezierPath(ovalIn: shapeLayer.bounds.insetBy(dx: inset, dy: inset)).cgPath
136 | }
137 |
138 | }
139 |
--------------------------------------------------------------------------------
/DGElasticPullToRefresh/DGElasticPullToRefreshExtensions.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | The MIT License (MIT)
4 |
5 | Copyright (c) 2015 Danil Gontovnik
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy
8 | of this software and associated documentation files (the "Software"), to deal
9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | SOFTWARE.
24 |
25 | */
26 |
27 | import UIKit
28 | import ObjectiveC
29 |
30 | // MARK: -
31 | // MARK: (NSObject) Extension
32 |
33 | public extension NSObject {
34 |
35 | // MARK: -
36 | // MARK: Vars
37 |
38 | fileprivate struct dg_associatedKeys {
39 | static var observersArray = "observers"
40 | }
41 |
42 | fileprivate var dg_observers: [[String : NSObject]] {
43 | get {
44 | if let observers = objc_getAssociatedObject(self, &dg_associatedKeys.observersArray) as? [[String : NSObject]] {
45 | return observers
46 | } else {
47 | let observers = [[String : NSObject]]()
48 | self.dg_observers = observers
49 | return observers
50 | }
51 | } set {
52 | objc_setAssociatedObject(self, &dg_associatedKeys.observersArray, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
53 | }
54 | }
55 |
56 | // MARK: -
57 | // MARK: Methods
58 |
59 | public func dg_addObserver(_ observer: NSObject, forKeyPath keyPath: String) {
60 | let observerInfo = [keyPath : observer]
61 |
62 | if dg_observers.index(where: { $0 == observerInfo }) == nil {
63 | dg_observers.append(observerInfo)
64 | addObserver(observer, forKeyPath: keyPath, options: .new, context: nil)
65 | }
66 | }
67 |
68 | public func dg_removeObserver(_ observer: NSObject, forKeyPath keyPath: String) {
69 | let observerInfo = [keyPath : observer]
70 |
71 | if let index = dg_observers.index(where: { $0 == observerInfo}) {
72 | dg_observers.remove(at: index)
73 | removeObserver(observer, forKeyPath: keyPath)
74 | }
75 | }
76 |
77 | }
78 |
79 | // MARK: -
80 | // MARK: (UIScrollView) Extension
81 |
82 | public extension UIScrollView {
83 |
84 | // MARK: - Vars
85 |
86 | fileprivate struct dg_associatedKeys {
87 | static var pullToRefreshView = "pullToRefreshView"
88 | }
89 |
90 | fileprivate var pullToRefreshView: DGElasticPullToRefreshView? {
91 | get {
92 | return objc_getAssociatedObject(self, &dg_associatedKeys.pullToRefreshView) as? DGElasticPullToRefreshView
93 | }
94 |
95 | set {
96 | objc_setAssociatedObject(self, &dg_associatedKeys.pullToRefreshView, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
97 | }
98 | }
99 |
100 | // MARK: - Methods (Public)
101 |
102 | public func dg_addPullToRefreshWithActionHandler(_ actionHandler: @escaping () -> Void, loadingView: DGElasticPullToRefreshLoadingView?) {
103 | isMultipleTouchEnabled = false
104 | panGestureRecognizer.maximumNumberOfTouches = 1
105 |
106 | let pullToRefreshView = DGElasticPullToRefreshView()
107 | self.pullToRefreshView = pullToRefreshView
108 | pullToRefreshView.actionHandler = actionHandler
109 | pullToRefreshView.loadingView = loadingView
110 | addSubview(pullToRefreshView)
111 |
112 | pullToRefreshView.observing = true
113 | }
114 |
115 | public func dg_removePullToRefresh() {
116 | pullToRefreshView?.disassociateDisplayLink()
117 | pullToRefreshView?.observing = false
118 | pullToRefreshView?.removeFromSuperview()
119 | }
120 |
121 | public func dg_setPullToRefreshBackgroundColor(_ color: UIColor) {
122 | pullToRefreshView?.backgroundColor = color
123 | }
124 |
125 | public func dg_setPullToRefreshFillColor(_ color: UIColor) {
126 | pullToRefreshView?.fillColor = color
127 | }
128 |
129 | public func dg_stopLoading() {
130 | pullToRefreshView?.stopLoading()
131 | }
132 | }
133 |
134 | // MARK: -
135 | // MARK: (UIView) Extension
136 |
137 | public extension UIView {
138 | func dg_center(_ usePresentationLayerIfPossible: Bool) -> CGPoint {
139 | if usePresentationLayerIfPossible, let presentationLayer = layer.presentation() {
140 | // Position can be used as a center, because anchorPoint is (0.5, 0.5)
141 | return presentationLayer.position
142 | }
143 | return center
144 | }
145 | }
146 |
147 | // MARK: -
148 | // MARK: (UIPanGestureRecognizer) Extension
149 |
150 | public extension UIPanGestureRecognizer {
151 | func dg_resign() {
152 | isEnabled = false
153 | isEnabled = true
154 | }
155 | }
156 |
157 | // MARK: -
158 | // MARK: (UIGestureRecognizerState) Extension
159 |
160 | public extension UIGestureRecognizerState {
161 | func dg_isAnyOf(_ values: [UIGestureRecognizerState]) -> Bool {
162 | return values.contains(where: { $0 == self })
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/DGElasticPullToRefreshExample.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 46;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 05B92F8C1BD25E7B006D60FB /* DGElasticPullToRefreshConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05B92F871BD25E7B006D60FB /* DGElasticPullToRefreshConstants.swift */; };
11 | 05B92F8D1BD25E7B006D60FB /* DGElasticPullToRefreshExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05B92F881BD25E7B006D60FB /* DGElasticPullToRefreshExtensions.swift */; };
12 | 05B92F8E1BD25E7B006D60FB /* DGElasticPullToRefreshLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05B92F891BD25E7B006D60FB /* DGElasticPullToRefreshLoadingView.swift */; };
13 | 05B92F8F1BD25E7B006D60FB /* DGElasticPullToRefreshLoadingViewCircle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05B92F8A1BD25E7B006D60FB /* DGElasticPullToRefreshLoadingViewCircle.swift */; };
14 | 05B92F901BD25E7B006D60FB /* DGElasticPullToRefreshView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05B92F8B1BD25E7B006D60FB /* DGElasticPullToRefreshView.swift */; };
15 | 05CD14691BBE8FEA00AF4030 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05CD14681BBE8FEA00AF4030 /* AppDelegate.swift */; };
16 | 05CD146B1BBE8FEA00AF4030 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05CD146A1BBE8FEA00AF4030 /* ViewController.swift */; };
17 | 05CD14701BBE8FEA00AF4030 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 05CD146F1BBE8FEA00AF4030 /* Assets.xcassets */; };
18 | 05CD14731BBE8FEA00AF4030 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 05CD14711BBE8FEA00AF4030 /* LaunchScreen.storyboard */; };
19 | 05FAB8BE1BBEF115004922F1 /* NavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05FAB8BD1BBEF115004922F1 /* NavigationController.swift */; };
20 | /* End PBXBuildFile section */
21 |
22 | /* Begin PBXFileReference section */
23 | 05B92F871BD25E7B006D60FB /* DGElasticPullToRefreshConstants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DGElasticPullToRefreshConstants.swift; sourceTree = ""; };
24 | 05B92F881BD25E7B006D60FB /* DGElasticPullToRefreshExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DGElasticPullToRefreshExtensions.swift; sourceTree = ""; };
25 | 05B92F891BD25E7B006D60FB /* DGElasticPullToRefreshLoadingView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DGElasticPullToRefreshLoadingView.swift; sourceTree = ""; };
26 | 05B92F8A1BD25E7B006D60FB /* DGElasticPullToRefreshLoadingViewCircle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DGElasticPullToRefreshLoadingViewCircle.swift; sourceTree = ""; };
27 | 05B92F8B1BD25E7B006D60FB /* DGElasticPullToRefreshView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DGElasticPullToRefreshView.swift; sourceTree = ""; };
28 | 05CD14651BBE8FEA00AF4030 /* DGElasticPullToRefreshExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DGElasticPullToRefreshExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
29 | 05CD14681BBE8FEA00AF4030 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
30 | 05CD146A1BBE8FEA00AF4030 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; };
31 | 05CD146F1BBE8FEA00AF4030 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
32 | 05CD14721BBE8FEA00AF4030 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
33 | 05CD14741BBE8FEA00AF4030 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
34 | 05FAB8BD1BBEF115004922F1 /* NavigationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationController.swift; sourceTree = ""; };
35 | /* End PBXFileReference section */
36 |
37 | /* Begin PBXFrameworksBuildPhase section */
38 | 05CD14621BBE8FEA00AF4030 /* Frameworks */ = {
39 | isa = PBXFrameworksBuildPhase;
40 | buildActionMask = 2147483647;
41 | files = (
42 | );
43 | runOnlyForDeploymentPostprocessing = 0;
44 | };
45 | /* End PBXFrameworksBuildPhase section */
46 |
47 | /* Begin PBXGroup section */
48 | 05B92F861BD25E7B006D60FB /* DGElasticPullToRefresh */ = {
49 | isa = PBXGroup;
50 | children = (
51 | 05B92F871BD25E7B006D60FB /* DGElasticPullToRefreshConstants.swift */,
52 | 05B92F881BD25E7B006D60FB /* DGElasticPullToRefreshExtensions.swift */,
53 | 05B92F891BD25E7B006D60FB /* DGElasticPullToRefreshLoadingView.swift */,
54 | 05B92F8A1BD25E7B006D60FB /* DGElasticPullToRefreshLoadingViewCircle.swift */,
55 | 05B92F8B1BD25E7B006D60FB /* DGElasticPullToRefreshView.swift */,
56 | );
57 | path = DGElasticPullToRefresh;
58 | sourceTree = SOURCE_ROOT;
59 | };
60 | 05CD145C1BBE8FEA00AF4030 = {
61 | isa = PBXGroup;
62 | children = (
63 | 05CD14671BBE8FEA00AF4030 /* DGElasticPullToRefreshExample */,
64 | 05CD14661BBE8FEA00AF4030 /* Products */,
65 | );
66 | sourceTree = "";
67 | };
68 | 05CD14661BBE8FEA00AF4030 /* Products */ = {
69 | isa = PBXGroup;
70 | children = (
71 | 05CD14651BBE8FEA00AF4030 /* DGElasticPullToRefreshExample.app */,
72 | );
73 | name = Products;
74 | sourceTree = "";
75 | };
76 | 05CD14671BBE8FEA00AF4030 /* DGElasticPullToRefreshExample */ = {
77 | isa = PBXGroup;
78 | children = (
79 | 05B92F861BD25E7B006D60FB /* DGElasticPullToRefresh */,
80 | 05CD14681BBE8FEA00AF4030 /* AppDelegate.swift */,
81 | 05CD146A1BBE8FEA00AF4030 /* ViewController.swift */,
82 | 05FAB8BD1BBEF115004922F1 /* NavigationController.swift */,
83 | 05CD146F1BBE8FEA00AF4030 /* Assets.xcassets */,
84 | 05CD14711BBE8FEA00AF4030 /* LaunchScreen.storyboard */,
85 | 05CD14741BBE8FEA00AF4030 /* Info.plist */,
86 | );
87 | path = DGElasticPullToRefreshExample;
88 | sourceTree = "";
89 | };
90 | /* End PBXGroup section */
91 |
92 | /* Begin PBXNativeTarget section */
93 | 05CD14641BBE8FEA00AF4030 /* DGElasticPullToRefreshExample */ = {
94 | isa = PBXNativeTarget;
95 | buildConfigurationList = 05CD14771BBE8FEA00AF4030 /* Build configuration list for PBXNativeTarget "DGElasticPullToRefreshExample" */;
96 | buildPhases = (
97 | 05CD14611BBE8FEA00AF4030 /* Sources */,
98 | 05CD14621BBE8FEA00AF4030 /* Frameworks */,
99 | 05CD14631BBE8FEA00AF4030 /* Resources */,
100 | );
101 | buildRules = (
102 | );
103 | dependencies = (
104 | );
105 | name = DGElasticPullToRefreshExample;
106 | productName = DGElasticPullToRefreshExample;
107 | productReference = 05CD14651BBE8FEA00AF4030 /* DGElasticPullToRefreshExample.app */;
108 | productType = "com.apple.product-type.application";
109 | };
110 | /* End PBXNativeTarget section */
111 |
112 | /* Begin PBXProject section */
113 | 05CD145D1BBE8FEA00AF4030 /* Project object */ = {
114 | isa = PBXProject;
115 | attributes = {
116 | LastUpgradeCheck = 0700;
117 | ORGANIZATIONNAME = "Danil Gontovnik";
118 | TargetAttributes = {
119 | 05CD14641BBE8FEA00AF4030 = {
120 | CreatedOnToolsVersion = 7.0;
121 | LastSwiftMigration = 0800;
122 | };
123 | };
124 | };
125 | buildConfigurationList = 05CD14601BBE8FEA00AF4030 /* Build configuration list for PBXProject "DGElasticPullToRefreshExample" */;
126 | compatibilityVersion = "Xcode 3.2";
127 | developmentRegion = English;
128 | hasScannedForEncodings = 0;
129 | knownRegions = (
130 | en,
131 | Base,
132 | );
133 | mainGroup = 05CD145C1BBE8FEA00AF4030;
134 | productRefGroup = 05CD14661BBE8FEA00AF4030 /* Products */;
135 | projectDirPath = "";
136 | projectRoot = "";
137 | targets = (
138 | 05CD14641BBE8FEA00AF4030 /* DGElasticPullToRefreshExample */,
139 | );
140 | };
141 | /* End PBXProject section */
142 |
143 | /* Begin PBXResourcesBuildPhase section */
144 | 05CD14631BBE8FEA00AF4030 /* Resources */ = {
145 | isa = PBXResourcesBuildPhase;
146 | buildActionMask = 2147483647;
147 | files = (
148 | 05CD14731BBE8FEA00AF4030 /* LaunchScreen.storyboard in Resources */,
149 | 05CD14701BBE8FEA00AF4030 /* Assets.xcassets in Resources */,
150 | );
151 | runOnlyForDeploymentPostprocessing = 0;
152 | };
153 | /* End PBXResourcesBuildPhase section */
154 |
155 | /* Begin PBXSourcesBuildPhase section */
156 | 05CD14611BBE8FEA00AF4030 /* Sources */ = {
157 | isa = PBXSourcesBuildPhase;
158 | buildActionMask = 2147483647;
159 | files = (
160 | 05B92F8C1BD25E7B006D60FB /* DGElasticPullToRefreshConstants.swift in Sources */,
161 | 05CD146B1BBE8FEA00AF4030 /* ViewController.swift in Sources */,
162 | 05B92F901BD25E7B006D60FB /* DGElasticPullToRefreshView.swift in Sources */,
163 | 05B92F8E1BD25E7B006D60FB /* DGElasticPullToRefreshLoadingView.swift in Sources */,
164 | 05FAB8BE1BBEF115004922F1 /* NavigationController.swift in Sources */,
165 | 05B92F8F1BD25E7B006D60FB /* DGElasticPullToRefreshLoadingViewCircle.swift in Sources */,
166 | 05CD14691BBE8FEA00AF4030 /* AppDelegate.swift in Sources */,
167 | 05B92F8D1BD25E7B006D60FB /* DGElasticPullToRefreshExtensions.swift in Sources */,
168 | );
169 | runOnlyForDeploymentPostprocessing = 0;
170 | };
171 | /* End PBXSourcesBuildPhase section */
172 |
173 | /* Begin PBXVariantGroup section */
174 | 05CD14711BBE8FEA00AF4030 /* LaunchScreen.storyboard */ = {
175 | isa = PBXVariantGroup;
176 | children = (
177 | 05CD14721BBE8FEA00AF4030 /* Base */,
178 | );
179 | name = LaunchScreen.storyboard;
180 | sourceTree = "";
181 | };
182 | /* End PBXVariantGroup section */
183 |
184 | /* Begin XCBuildConfiguration section */
185 | 05CD14751BBE8FEA00AF4030 /* Debug */ = {
186 | isa = XCBuildConfiguration;
187 | buildSettings = {
188 | ALWAYS_SEARCH_USER_PATHS = NO;
189 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
190 | CLANG_CXX_LIBRARY = "libc++";
191 | CLANG_ENABLE_MODULES = YES;
192 | CLANG_ENABLE_OBJC_ARC = YES;
193 | CLANG_WARN_BOOL_CONVERSION = YES;
194 | CLANG_WARN_CONSTANT_CONVERSION = YES;
195 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
196 | CLANG_WARN_EMPTY_BODY = YES;
197 | CLANG_WARN_ENUM_CONVERSION = YES;
198 | CLANG_WARN_INT_CONVERSION = YES;
199 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
200 | CLANG_WARN_UNREACHABLE_CODE = YES;
201 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
202 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
203 | COPY_PHASE_STRIP = NO;
204 | DEBUG_INFORMATION_FORMAT = dwarf;
205 | ENABLE_STRICT_OBJC_MSGSEND = YES;
206 | ENABLE_TESTABILITY = YES;
207 | GCC_C_LANGUAGE_STANDARD = gnu99;
208 | GCC_DYNAMIC_NO_PIC = NO;
209 | GCC_NO_COMMON_BLOCKS = YES;
210 | GCC_OPTIMIZATION_LEVEL = 0;
211 | GCC_PREPROCESSOR_DEFINITIONS = (
212 | "DEBUG=1",
213 | "$(inherited)",
214 | );
215 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
216 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
217 | GCC_WARN_UNDECLARED_SELECTOR = YES;
218 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
219 | GCC_WARN_UNUSED_FUNCTION = YES;
220 | GCC_WARN_UNUSED_VARIABLE = YES;
221 | IPHONEOS_DEPLOYMENT_TARGET = 8.0;
222 | MTL_ENABLE_DEBUG_INFO = YES;
223 | ONLY_ACTIVE_ARCH = YES;
224 | SDKROOT = iphoneos;
225 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
226 | TARGETED_DEVICE_FAMILY = "1,2";
227 | };
228 | name = Debug;
229 | };
230 | 05CD14761BBE8FEA00AF4030 /* Release */ = {
231 | isa = XCBuildConfiguration;
232 | buildSettings = {
233 | ALWAYS_SEARCH_USER_PATHS = NO;
234 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
235 | CLANG_CXX_LIBRARY = "libc++";
236 | CLANG_ENABLE_MODULES = YES;
237 | CLANG_ENABLE_OBJC_ARC = YES;
238 | CLANG_WARN_BOOL_CONVERSION = YES;
239 | CLANG_WARN_CONSTANT_CONVERSION = YES;
240 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
241 | CLANG_WARN_EMPTY_BODY = YES;
242 | CLANG_WARN_ENUM_CONVERSION = YES;
243 | CLANG_WARN_INT_CONVERSION = YES;
244 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
245 | CLANG_WARN_UNREACHABLE_CODE = YES;
246 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
247 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
248 | COPY_PHASE_STRIP = NO;
249 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
250 | ENABLE_NS_ASSERTIONS = NO;
251 | ENABLE_STRICT_OBJC_MSGSEND = YES;
252 | GCC_C_LANGUAGE_STANDARD = gnu99;
253 | GCC_NO_COMMON_BLOCKS = YES;
254 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
255 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
256 | GCC_WARN_UNDECLARED_SELECTOR = YES;
257 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
258 | GCC_WARN_UNUSED_FUNCTION = YES;
259 | GCC_WARN_UNUSED_VARIABLE = YES;
260 | IPHONEOS_DEPLOYMENT_TARGET = 8.0;
261 | MTL_ENABLE_DEBUG_INFO = NO;
262 | SDKROOT = iphoneos;
263 | TARGETED_DEVICE_FAMILY = "1,2";
264 | VALIDATE_PRODUCT = YES;
265 | };
266 | name = Release;
267 | };
268 | 05CD14781BBE8FEA00AF4030 /* Debug */ = {
269 | isa = XCBuildConfiguration;
270 | buildSettings = {
271 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
272 | INFOPLIST_FILE = DGElasticPullToRefreshExample/Info.plist;
273 | IPHONEOS_DEPLOYMENT_TARGET = 8.0;
274 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
275 | PRODUCT_BUNDLE_IDENTIFIER = com.gatafan.DGElasticPullToRefreshExample;
276 | PRODUCT_NAME = DGElasticPullToRefreshExample;
277 | SWIFT_VERSION = 3.0;
278 | };
279 | name = Debug;
280 | };
281 | 05CD14791BBE8FEA00AF4030 /* Release */ = {
282 | isa = XCBuildConfiguration;
283 | buildSettings = {
284 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
285 | INFOPLIST_FILE = DGElasticPullToRefreshExample/Info.plist;
286 | IPHONEOS_DEPLOYMENT_TARGET = 8.0;
287 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
288 | PRODUCT_BUNDLE_IDENTIFIER = com.gatafan.DGElasticPullToRefreshExample;
289 | PRODUCT_NAME = DGElasticPullToRefreshExample;
290 | SWIFT_VERSION = 3.0;
291 | };
292 | name = Release;
293 | };
294 | /* End XCBuildConfiguration section */
295 |
296 | /* Begin XCConfigurationList section */
297 | 05CD14601BBE8FEA00AF4030 /* Build configuration list for PBXProject "DGElasticPullToRefreshExample" */ = {
298 | isa = XCConfigurationList;
299 | buildConfigurations = (
300 | 05CD14751BBE8FEA00AF4030 /* Debug */,
301 | 05CD14761BBE8FEA00AF4030 /* Release */,
302 | );
303 | defaultConfigurationIsVisible = 0;
304 | defaultConfigurationName = Release;
305 | };
306 | 05CD14771BBE8FEA00AF4030 /* Build configuration list for PBXNativeTarget "DGElasticPullToRefreshExample" */ = {
307 | isa = XCConfigurationList;
308 | buildConfigurations = (
309 | 05CD14781BBE8FEA00AF4030 /* Debug */,
310 | 05CD14791BBE8FEA00AF4030 /* Release */,
311 | );
312 | defaultConfigurationIsVisible = 0;
313 | defaultConfigurationName = Release;
314 | };
315 | /* End XCConfigurationList section */
316 | };
317 | rootObject = 05CD145D1BBE8FEA00AF4030 /* Project object */;
318 | }
319 |
--------------------------------------------------------------------------------
/DGElasticPullToRefresh/DGElasticPullToRefreshView.swift:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | The MIT License (MIT)
4 |
5 | Copyright (c) 2015 Danil Gontovnik
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy
8 | of this software and associated documentation files (the "Software"), to deal
9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | SOFTWARE.
24 |
25 | */
26 |
27 | import UIKit
28 |
29 | // MARK: -
30 | // MARK: DGElasticPullToRefreshState
31 |
32 | public
33 | enum DGElasticPullToRefreshState: Int {
34 | case stopped
35 | case dragging
36 | case animatingBounce
37 | case loading
38 | case animatingToStopped
39 |
40 | func isAnyOf(_ values: [DGElasticPullToRefreshState]) -> Bool {
41 | return values.contains(where: { $0 == self })
42 | }
43 | }
44 |
45 | // MARK: -
46 | // MARK: DGElasticPullToRefreshView
47 |
48 | open class DGElasticPullToRefreshView: UIView {
49 |
50 | // MARK: -
51 | // MARK: Vars
52 |
53 | fileprivate var _state: DGElasticPullToRefreshState = .stopped
54 | fileprivate(set) var state: DGElasticPullToRefreshState {
55 | get { return _state }
56 | set {
57 | let previousValue = state
58 | _state = newValue
59 |
60 | if previousValue == .dragging && newValue == .animatingBounce {
61 | loadingView?.startAnimating()
62 | animateBounce()
63 | } else if newValue == .loading && actionHandler != nil {
64 | actionHandler()
65 | } else if newValue == .animatingToStopped {
66 | resetScrollViewContentInset(shouldAddObserverWhenFinished: true, animated: true, completion: { [weak self] () -> () in self?.state = .stopped })
67 | } else if newValue == .stopped {
68 | loadingView?.stopLoading()
69 | }
70 | }
71 | }
72 |
73 | fileprivate var originalContentInsetTop: CGFloat = 0.0 { didSet { layoutSubviews() } }
74 | fileprivate let shapeLayer = CAShapeLayer()
75 |
76 | fileprivate var displayLink: CADisplayLink!
77 |
78 | var actionHandler: (() -> Void)!
79 |
80 | var loadingView: DGElasticPullToRefreshLoadingView? {
81 | willSet {
82 | loadingView?.removeFromSuperview()
83 | if let newValue = newValue {
84 | addSubview(newValue)
85 | }
86 | }
87 | }
88 |
89 | var observing: Bool = false {
90 | didSet {
91 | guard let scrollView = scrollView() else { return }
92 | if observing {
93 | scrollView.dg_addObserver(self, forKeyPath: DGElasticPullToRefreshConstants.KeyPaths.ContentOffset)
94 | scrollView.dg_addObserver(self, forKeyPath: DGElasticPullToRefreshConstants.KeyPaths.ContentInset)
95 | scrollView.dg_addObserver(self, forKeyPath: DGElasticPullToRefreshConstants.KeyPaths.Frame)
96 | scrollView.dg_addObserver(self, forKeyPath: DGElasticPullToRefreshConstants.KeyPaths.PanGestureRecognizerState)
97 | } else {
98 | scrollView.dg_removeObserver(self, forKeyPath: DGElasticPullToRefreshConstants.KeyPaths.ContentOffset)
99 | scrollView.dg_removeObserver(self, forKeyPath: DGElasticPullToRefreshConstants.KeyPaths.ContentInset)
100 | scrollView.dg_removeObserver(self, forKeyPath: DGElasticPullToRefreshConstants.KeyPaths.Frame)
101 | scrollView.dg_removeObserver(self, forKeyPath: DGElasticPullToRefreshConstants.KeyPaths.PanGestureRecognizerState)
102 | }
103 | }
104 | }
105 |
106 | var fillColor: UIColor = .clear { didSet { shapeLayer.fillColor = fillColor.cgColor } }
107 |
108 | // MARK: Views
109 |
110 | fileprivate let bounceAnimationHelperView = UIView()
111 |
112 | fileprivate let cControlPointView = UIView()
113 | fileprivate let l1ControlPointView = UIView()
114 | fileprivate let l2ControlPointView = UIView()
115 | fileprivate let l3ControlPointView = UIView()
116 | fileprivate let r1ControlPointView = UIView()
117 | fileprivate let r2ControlPointView = UIView()
118 | fileprivate let r3ControlPointView = UIView()
119 |
120 | // MARK: -
121 | // MARK: Constructors
122 |
123 | init() {
124 | super.init(frame: CGRect.zero)
125 |
126 | displayLink = CADisplayLink(target: self, selector: #selector(DGElasticPullToRefreshView.displayLinkTick))
127 | displayLink.add(to: RunLoop.main, forMode: RunLoopMode.commonModes)
128 | displayLink.isPaused = true
129 |
130 | shapeLayer.backgroundColor = UIColor.clear.cgColor
131 | shapeLayer.fillColor = UIColor.black.cgColor
132 | shapeLayer.actions = ["path" : NSNull(), "position" : NSNull(), "bounds" : NSNull()]
133 | layer.addSublayer(shapeLayer)
134 |
135 | addSubview(bounceAnimationHelperView)
136 | addSubview(cControlPointView)
137 | addSubview(l1ControlPointView)
138 | addSubview(l2ControlPointView)
139 | addSubview(l3ControlPointView)
140 | addSubview(r1ControlPointView)
141 | addSubview(r2ControlPointView)
142 | addSubview(r3ControlPointView)
143 |
144 | NotificationCenter.default.addObserver(self, selector: #selector(DGElasticPullToRefreshView.applicationWillEnterForeground), name: NSNotification.Name.UIApplicationWillEnterForeground, object: nil)
145 | }
146 |
147 | required public init?(coder aDecoder: NSCoder) {
148 | fatalError("init(coder:) has not been implemented")
149 | }
150 |
151 | // MARK: -
152 |
153 | /**
154 | Has to be called when the receiver is no longer required. Otherwise the main loop holds a reference to the receiver which in turn will prevent the receiver from being deallocated.
155 | */
156 | func disassociateDisplayLink() {
157 | displayLink?.invalidate()
158 | }
159 |
160 | deinit {
161 | observing = false
162 | NotificationCenter.default.removeObserver(self)
163 | }
164 |
165 | // MARK: -
166 | // MARK: Observer
167 |
168 | override open func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
169 | if keyPath == DGElasticPullToRefreshConstants.KeyPaths.ContentOffset {
170 | if let newContentOffset = change?[NSKeyValueChangeKey.newKey], let scrollView = scrollView() {
171 | let newContentOffsetY = (newContentOffset as AnyObject).cgPointValue.y
172 | if state.isAnyOf([.loading, .animatingToStopped]) && newContentOffsetY < -scrollView.contentInset.top {
173 | scrollView.contentOffset.y = -scrollView.contentInset.top
174 | } else {
175 | scrollViewDidChangeContentOffset(dragging: scrollView.isDragging)
176 | }
177 | layoutSubviews()
178 | }
179 | } else if keyPath == DGElasticPullToRefreshConstants.KeyPaths.ContentInset {
180 | if let newContentInset = change?[NSKeyValueChangeKey.newKey] {
181 | let newContentInsetTop = (newContentInset as AnyObject).uiEdgeInsetsValue.top
182 | originalContentInsetTop = newContentInsetTop
183 | }
184 | } else if keyPath == DGElasticPullToRefreshConstants.KeyPaths.Frame {
185 | layoutSubviews()
186 | } else if keyPath == DGElasticPullToRefreshConstants.KeyPaths.PanGestureRecognizerState {
187 | if let gestureState = scrollView()?.panGestureRecognizer.state, gestureState.dg_isAnyOf([.ended, .cancelled, .failed]) {
188 | scrollViewDidChangeContentOffset(dragging: false)
189 | }
190 | }
191 | }
192 |
193 | // MARK: -
194 | // MARK: Notifications
195 |
196 | func applicationWillEnterForeground() {
197 | if state == .loading {
198 | layoutSubviews()
199 | }
200 | }
201 |
202 | // MARK: -
203 | // MARK: Methods (Public)
204 |
205 | fileprivate func scrollView() -> UIScrollView? {
206 | return superview as? UIScrollView
207 | }
208 |
209 | func stopLoading() {
210 | // Prevent stop close animation
211 | if state == .animatingToStopped {
212 | return
213 | }
214 | state = .animatingToStopped
215 | }
216 |
217 | // MARK: Methods (Private)
218 |
219 | fileprivate func isAnimating() -> Bool {
220 | return state.isAnyOf([.animatingBounce, .animatingToStopped])
221 | }
222 |
223 | fileprivate func actualContentOffsetY() -> CGFloat {
224 | guard let scrollView = scrollView() else { return 0.0 }
225 | return max(-scrollView.contentInset.top - scrollView.contentOffset.y, 0)
226 | }
227 |
228 | fileprivate func currentHeight() -> CGFloat {
229 | guard let scrollView = scrollView() else { return 0.0 }
230 | return max(-originalContentInsetTop - scrollView.contentOffset.y, 0)
231 | }
232 |
233 | fileprivate func currentWaveHeight() -> CGFloat {
234 | return min(bounds.height / 3.0 * 1.6, DGElasticPullToRefreshConstants.WaveMaxHeight)
235 | }
236 |
237 | fileprivate func currentPath() -> CGPath {
238 | let width: CGFloat = scrollView()?.bounds.width ?? 0.0
239 |
240 | let bezierPath = UIBezierPath()
241 | let animating = isAnimating()
242 |
243 | bezierPath.move(to: CGPoint(x: 0.0, y: 0.0))
244 | bezierPath.addLine(to: CGPoint(x: 0.0, y: l3ControlPointView.dg_center(animating).y))
245 | bezierPath.addCurve(to: l1ControlPointView.dg_center(animating), controlPoint1: l3ControlPointView.dg_center(animating), controlPoint2: l2ControlPointView.dg_center(animating))
246 | bezierPath.addCurve(to: r1ControlPointView.dg_center(animating), controlPoint1: cControlPointView.dg_center(animating), controlPoint2: r1ControlPointView.dg_center(animating))
247 | bezierPath.addCurve(to: r3ControlPointView.dg_center(animating), controlPoint1: r1ControlPointView.dg_center(animating), controlPoint2: r2ControlPointView.dg_center(animating))
248 | bezierPath.addLine(to: CGPoint(x: width, y: 0.0))
249 |
250 | bezierPath.close()
251 |
252 | return bezierPath.cgPath
253 | }
254 |
255 | fileprivate func scrollViewDidChangeContentOffset(dragging: Bool) {
256 | let offsetY = actualContentOffsetY()
257 |
258 | if state == .stopped && dragging {
259 | state = .dragging
260 | } else if state == .dragging && dragging == false {
261 | if offsetY >= DGElasticPullToRefreshConstants.MinOffsetToPull {
262 | state = .animatingBounce
263 | } else {
264 | state = .stopped
265 | }
266 | } else if state.isAnyOf([.dragging, .stopped]) {
267 | let pullProgress: CGFloat = offsetY / DGElasticPullToRefreshConstants.MinOffsetToPull
268 | loadingView?.setPullProgress(pullProgress)
269 | }
270 | }
271 |
272 | fileprivate func resetScrollViewContentInset(shouldAddObserverWhenFinished: Bool, animated: Bool, completion: (() -> ())?) {
273 | guard let scrollView = scrollView() else { return }
274 |
275 | var contentInset = scrollView.contentInset
276 | contentInset.top = originalContentInsetTop
277 |
278 | if state == .animatingBounce {
279 | contentInset.top += currentHeight()
280 | } else if state == .loading {
281 | contentInset.top += DGElasticPullToRefreshConstants.LoadingContentInset
282 | }
283 |
284 | scrollView.dg_removeObserver(self, forKeyPath: DGElasticPullToRefreshConstants.KeyPaths.ContentInset)
285 |
286 | let animationBlock = { scrollView.contentInset = contentInset }
287 | let completionBlock = { () -> Void in
288 | if shouldAddObserverWhenFinished && self.observing {
289 | scrollView.dg_addObserver(self, forKeyPath: DGElasticPullToRefreshConstants.KeyPaths.ContentInset)
290 | }
291 | completion?()
292 | }
293 |
294 | if animated {
295 | startDisplayLink()
296 | UIView.animate(withDuration: 0.4, animations: animationBlock, completion: { _ in
297 | self.stopDisplayLink()
298 | completionBlock()
299 | })
300 | } else {
301 | animationBlock()
302 | completionBlock()
303 | }
304 | }
305 |
306 | fileprivate func animateBounce()
307 | {
308 | guard let scrollView = scrollView() else { return }
309 | if (!self.observing) { return }
310 |
311 |
312 | resetScrollViewContentInset(shouldAddObserverWhenFinished: false, animated: false, completion: nil)
313 |
314 | let centerY = DGElasticPullToRefreshConstants.LoadingContentInset
315 | let duration = 0.9
316 |
317 | scrollView.isScrollEnabled = false
318 | startDisplayLink()
319 | scrollView.dg_removeObserver(self, forKeyPath: DGElasticPullToRefreshConstants.KeyPaths.ContentOffset)
320 | scrollView.dg_removeObserver(self, forKeyPath: DGElasticPullToRefreshConstants.KeyPaths.ContentInset)
321 | UIView.animate(withDuration: duration, delay: 0.0, usingSpringWithDamping: 0.43, initialSpringVelocity: 0.0, options: [], animations: { [weak self] in
322 | self?.cControlPointView.center.y = centerY
323 | self?.l1ControlPointView.center.y = centerY
324 | self?.l2ControlPointView.center.y = centerY
325 | self?.l3ControlPointView.center.y = centerY
326 | self?.r1ControlPointView.center.y = centerY
327 | self?.r2ControlPointView.center.y = centerY
328 | self?.r3ControlPointView.center.y = centerY
329 | }, completion: { [weak self] _ in
330 | self?.stopDisplayLink()
331 | self?.resetScrollViewContentInset(shouldAddObserverWhenFinished: true, animated: false, completion: nil)
332 | if let strongSelf = self, let scrollView = strongSelf.scrollView() {
333 | scrollView.dg_addObserver(strongSelf, forKeyPath: DGElasticPullToRefreshConstants.KeyPaths.ContentOffset)
334 | scrollView.isScrollEnabled = true
335 | }
336 | self?.state = .loading
337 | })
338 |
339 | bounceAnimationHelperView.center = CGPoint(x: 0.0, y: originalContentInsetTop + currentHeight())
340 | UIView.animate(withDuration: duration * 0.4, animations: { [weak self] in
341 | if let contentInsetTop = self?.originalContentInsetTop {
342 | self?.bounceAnimationHelperView.center = CGPoint(x: 0.0, y: contentInsetTop + DGElasticPullToRefreshConstants.LoadingContentInset)
343 | }
344 | }, completion: nil)
345 | }
346 |
347 | // MARK: -
348 | // MARK: CADisplayLink
349 |
350 | fileprivate func startDisplayLink() {
351 | displayLink.isPaused = false
352 | }
353 |
354 | fileprivate func stopDisplayLink() {
355 | displayLink.isPaused = true
356 | }
357 |
358 | func displayLinkTick() {
359 | let width = bounds.width
360 | var height: CGFloat = 0.0
361 |
362 | if state == .animatingBounce {
363 | guard let scrollView = scrollView() else { return }
364 |
365 | scrollView.contentInset.top = bounceAnimationHelperView.dg_center(isAnimating()).y
366 | scrollView.contentOffset.y = -scrollView.contentInset.top
367 |
368 | height = scrollView.contentInset.top - originalContentInsetTop
369 |
370 | frame = CGRect(x: 0.0, y: -height - 1.0, width: width, height: height)
371 | } else if state == .animatingToStopped {
372 | height = actualContentOffsetY()
373 | }
374 |
375 | shapeLayer.frame = CGRect(x: 0.0, y: 0.0, width: width, height: height)
376 | shapeLayer.path = currentPath()
377 |
378 | layoutLoadingView()
379 | }
380 |
381 | // MARK: -
382 | // MARK: Layout
383 |
384 | fileprivate func layoutLoadingView() {
385 | let width = bounds.width
386 | let height: CGFloat = bounds.height
387 |
388 | let loadingViewSize: CGFloat = DGElasticPullToRefreshConstants.LoadingViewSize
389 | let minOriginY = (DGElasticPullToRefreshConstants.LoadingContentInset - loadingViewSize) / 2.0
390 | let originY: CGFloat = max(min((height - loadingViewSize) / 2.0, minOriginY), 0.0)
391 |
392 | loadingView?.frame = CGRect(x: (width - loadingViewSize) / 2.0, y: originY, width: loadingViewSize, height: loadingViewSize)
393 | loadingView?.maskLayer.frame = convert(shapeLayer.frame, to: loadingView)
394 | loadingView?.maskLayer.path = shapeLayer.path
395 | }
396 |
397 | override open func layoutSubviews() {
398 | super.layoutSubviews()
399 |
400 | if let scrollView = scrollView() , state != .animatingBounce {
401 | let width = scrollView.bounds.width
402 | let height = currentHeight()
403 |
404 | frame = CGRect(x: 0.0, y: -height, width: width, height: height)
405 |
406 | if state.isAnyOf([.loading, .animatingToStopped]) {
407 | cControlPointView.center = CGPoint(x: width / 2.0, y: height)
408 | l1ControlPointView.center = CGPoint(x: 0.0, y: height)
409 | l2ControlPointView.center = CGPoint(x: 0.0, y: height)
410 | l3ControlPointView.center = CGPoint(x: 0.0, y: height)
411 | r1ControlPointView.center = CGPoint(x: width, y: height)
412 | r2ControlPointView.center = CGPoint(x: width, y: height)
413 | r3ControlPointView.center = CGPoint(x: width, y: height)
414 | } else {
415 | let locationX = scrollView.panGestureRecognizer.location(in: scrollView).x
416 |
417 | let waveHeight = currentWaveHeight()
418 | let baseHeight = bounds.height - waveHeight
419 |
420 | let minLeftX = min((locationX - width / 2.0) * 0.28, 0.0)
421 | let maxRightX = max(width + (locationX - width / 2.0) * 0.28, width)
422 |
423 | let leftPartWidth = locationX - minLeftX
424 | let rightPartWidth = maxRightX - locationX
425 |
426 | cControlPointView.center = CGPoint(x: locationX , y: baseHeight + waveHeight * 1.36)
427 | l1ControlPointView.center = CGPoint(x: minLeftX + leftPartWidth * 0.71, y: baseHeight + waveHeight * 0.64)
428 | l2ControlPointView.center = CGPoint(x: minLeftX + leftPartWidth * 0.44, y: baseHeight)
429 | l3ControlPointView.center = CGPoint(x: minLeftX, y: baseHeight)
430 | r1ControlPointView.center = CGPoint(x: maxRightX - rightPartWidth * 0.71, y: baseHeight + waveHeight * 0.64)
431 | r2ControlPointView.center = CGPoint(x: maxRightX - (rightPartWidth * 0.44), y: baseHeight)
432 | r3ControlPointView.center = CGPoint(x: maxRightX, y: baseHeight)
433 | }
434 |
435 | shapeLayer.frame = CGRect(x: 0.0, y: 0.0, width: width, height: height)
436 | shapeLayer.path = currentPath()
437 |
438 | layoutLoadingView()
439 | }
440 | }
441 |
442 | }
443 |
--------------------------------------------------------------------------------