├── LICENSE
├── README.md
├── sample.gif
└── sample.playground
├── Contents.swift
├── contents.xcplayground
├── playground.xcworkspace
└── contents.xcworkspacedata
└── timeline.xctimeline
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Tueno
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # MaterialCircularProgress
2 | Material like circular progress animation sample for iOS.
3 |
4 | * Written by Swift 3 (Thanks @RuudPuts!)
5 | If you want to use this on Swift 2, checkout the commit de25b549cb8b9c2dd1935fb8a9c745aa83b53eae.
6 | * You can copy classes from sample.playground for use in your project.
7 |
8 | 
9 |
--------------------------------------------------------------------------------
/sample.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tueno/MaterialCircularProgress/b2b55bd2a43f49b882856de9cfe695cdf80d484f/sample.gif
--------------------------------------------------------------------------------
/sample.playground/Contents.swift:
--------------------------------------------------------------------------------
1 | //: Playground - noun: a place where people can play
2 |
3 | import UIKit
4 | import PlaygroundSupport
5 |
6 | extension UIColor {
7 |
8 | convenience init(hex: UInt, alpha: CGFloat) {
9 | self.init(
10 | red: CGFloat((hex & 0xFF0000) >> 16) / 255.0,
11 | green: CGFloat((hex & 0x00FF00) >> 8) / 255.0,
12 | blue: CGFloat(hex & 0x0000FF) / 255.0,
13 | alpha: CGFloat(alpha)
14 | )
15 | }
16 |
17 | }
18 |
19 | final class MaterialCircularProgress: UIView {
20 |
21 | let MinStrokeLength: CGFloat = 0.01
22 | let MaxStrokeLength: CGFloat = 1.0
23 | let circleOutlineLayer = CAShapeLayer()
24 | let insideCircleShapeLayer = CAShapeLayer()
25 | let checkmarkShapeLayer = CAShapeLayer()
26 | let AfterpartDuration: Double = 0.3
27 |
28 | var CheckmarkPath: UIBezierPath {
29 | get {
30 | let CheckmarkSize = CGSize(width: 20, height: 16)
31 | let checkmarkPath = UIBezierPath()
32 | let startPoint = CGPoint(x: bounds.width * 0.5 - CheckmarkSize.width * 0.48,
33 | y: bounds.height * 0.5 + CheckmarkSize.height * 0.05)
34 | checkmarkPath.move(to: startPoint)
35 | let firstLineEndPoint = CGPoint(x: startPoint.x + CheckmarkSize.width * 0.36,
36 | y: startPoint.y + CheckmarkSize.height * 0.36)
37 | checkmarkPath.addLine(to: firstLineEndPoint)
38 | let secondLineEndPoint = CGPoint(x: firstLineEndPoint.x + CheckmarkSize.width * 0.64,
39 | y: firstLineEndPoint.y - CheckmarkSize.height)
40 | checkmarkPath.addLine(to: secondLineEndPoint)
41 | return checkmarkPath
42 | }
43 | }
44 |
45 | var duration: Double = 3.0
46 |
47 | override init(frame: CGRect) {
48 | super.init(frame: frame)
49 | backgroundColor = .clear
50 | initShapeLayer()
51 | }
52 |
53 | required init?(coder aDecoder: NSCoder) {
54 | fatalError("init(coder:) has not been implemented")
55 | }
56 |
57 | func initShapeLayer() {
58 | // Outline
59 | let outLineWidth: CGFloat = 5
60 | circleOutlineLayer.actions = ["strokeEnd" : NSNull(),
61 | "strokeStart" : NSNull(),
62 | "transform" : NSNull(),
63 | "strokeColor" : NSNull()]
64 | circleOutlineLayer.backgroundColor = UIColor.clear.cgColor
65 | circleOutlineLayer.strokeColor = UIColor.blue.cgColor
66 | circleOutlineLayer.fillColor = UIColor.clear.cgColor
67 | circleOutlineLayer.lineWidth = outLineWidth
68 | circleOutlineLayer.strokeStart = 0
69 | circleOutlineLayer.strokeEnd = MinStrokeLength
70 | let center = CGPoint(x: bounds.width*0.5, y: bounds.height*0.5)
71 | circleOutlineLayer.frame = bounds
72 | circleOutlineLayer.lineCap = kCALineCapButt
73 | circleOutlineLayer.path = UIBezierPath(arcCenter: center,
74 | radius: center.x,
75 | startAngle: 0,
76 | endAngle: CGFloat(M_PI*2),
77 | clockwise: true).cgPath
78 | circleOutlineLayer.transform = CATransform3DMakeRotation(CGFloat(M_PI*1.5), 0, 0, 1.0)
79 | layer.addSublayer(circleOutlineLayer)
80 | // Inside
81 | let insideCircleRect = CGRect(origin: CGPoint(x: outLineWidth * 0.5, y: outLineWidth * 0.5),
82 | size: CGSize(width: circleOutlineLayer.bounds.width - outLineWidth,
83 | height: circleOutlineLayer.bounds.height - outLineWidth))
84 | let insideCirclePath = UIBezierPath(ovalIn: insideCircleRect).cgPath
85 | insideCircleShapeLayer.path = insideCirclePath
86 | insideCircleShapeLayer.fillColor = UIColor(hex: 0xf19b00, alpha: 1.0).cgColor
87 | insideCircleShapeLayer.opacity = 0
88 | layer.addSublayer(insideCircleShapeLayer)
89 | // Checkmark
90 | checkmarkShapeLayer.strokeColor = UIColor.white.cgColor
91 | checkmarkShapeLayer.lineWidth = 3.0
92 | checkmarkShapeLayer.fillColor = UIColor.clear.cgColor
93 | checkmarkShapeLayer.path = CheckmarkPath.cgPath
94 | checkmarkShapeLayer.strokeEnd = 0
95 | layer.addSublayer(checkmarkShapeLayer)
96 | }
97 |
98 | func startAnimating(duration: Double) {
99 | self.duration = duration
100 | if layer.animation(forKey: "rotation") == nil {
101 | startColorAnimation()
102 | startStrokeAnimation()
103 | startRotatingAnimation()
104 | }
105 | }
106 |
107 | private func startColorAnimation() {
108 | let color = CAKeyframeAnimation(keyPath: "strokeColor")
109 | color.duration = 10.0
110 | color.values = [UIColor(hex: 0xf19b00, alpha: 1.0).cgColor]
111 | color.calculationMode = kCAAnimationPaced
112 | color.repeatCount = Float.infinity
113 | circleOutlineLayer.add(color, forKey: "color")
114 | }
115 |
116 | private func startRotatingAnimation() {
117 | let rotation = CABasicAnimation(keyPath: "transform.rotation.z")
118 | rotation.toValue = M_PI*6.0
119 | rotation.duration = (duration - AfterpartDuration) * 0.77
120 | rotation.isCumulative = true
121 | rotation.isAdditive = true
122 | rotation.isRemovedOnCompletion = false
123 | rotation.fillMode = kCAFillModeForwards
124 | rotation.timingFunction = CAMediaTimingFunction(controlPoints: 0.39, 0.575, 0.565, 1.0)
125 | circleOutlineLayer.add(rotation, forKey: "rotation")
126 | }
127 |
128 | private func startStrokeAnimation() {
129 | let easeInOutSineTimingFunc = CAMediaTimingFunction(controlPoints: 0.39, 0.575, 0.565, 1.0)
130 | let progress: CGFloat = MaxStrokeLength
131 | let endFromValue: CGFloat = circleOutlineLayer.strokeEnd
132 | let endToValue: CGFloat = endFromValue + progress
133 | let strokeEnd = CABasicAnimation(keyPath: "strokeEnd")
134 | strokeEnd.fromValue = endFromValue
135 | strokeEnd.toValue = endToValue
136 | strokeEnd.duration = duration - AfterpartDuration
137 | strokeEnd.fillMode = kCAFillModeForwards
138 | strokeEnd.timingFunction = easeInOutSineTimingFunc
139 | strokeEnd.isRemovedOnCompletion = false
140 | let pathAnim = CAAnimationGroup()
141 | pathAnim.animations = [strokeEnd]
142 | pathAnim.duration = duration - AfterpartDuration
143 | pathAnim.fillMode = kCAFillModeForwards
144 | pathAnim.isRemovedOnCompletion = false
145 | CATransaction.begin()
146 | CATransaction.setCompletionBlock {
147 | self.startCompletionAnimation()
148 | }
149 | circleOutlineLayer.add(pathAnim, forKey: "stroke")
150 | CATransaction.commit()
151 | }
152 |
153 | private func startCompletionAnimation() {
154 | startFadeOutOutSideLineAnimation()
155 | startFillCircleAnimation()
156 | startDrawingCheckmarkAnimation()
157 | }
158 |
159 | private func startFadeOutOutSideLineAnimation() {
160 | let fadeOutAnimation = CABasicAnimation(keyPath: "opacity")
161 | fadeOutAnimation.toValue = 0
162 | fadeOutAnimation.duration = AfterpartDuration
163 | fadeOutAnimation.fillMode = kCAFillModeForwards
164 | fadeOutAnimation.isRemovedOnCompletion = false
165 | fadeOutAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
166 | circleOutlineLayer.add(fadeOutAnimation, forKey: "fadeOut")
167 | }
168 |
169 | private func startFillCircleAnimation() {
170 | let fadeInAnimation = CABasicAnimation(keyPath: "opacity")
171 | fadeInAnimation.toValue = 1.0
172 | fadeInAnimation.duration = AfterpartDuration
173 | fadeInAnimation.fillMode = kCAFillModeForwards
174 | fadeInAnimation.isRemovedOnCompletion = false
175 | fadeInAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
176 | insideCircleShapeLayer.add(fadeInAnimation, forKey: "fadeOut")
177 | }
178 |
179 | private func startDrawingCheckmarkAnimation() {
180 | let drawPathAnimation = CABasicAnimation(keyPath: "strokeEnd")
181 | drawPathAnimation.toValue = 1.0
182 | drawPathAnimation.fillMode = kCAFillModeForwards
183 | drawPathAnimation.isRemovedOnCompletion = false
184 | drawPathAnimation.duration = AfterpartDuration
185 | checkmarkShapeLayer.add(drawPathAnimation, forKey: "strokeEnd")
186 | }
187 |
188 | func stopAnimating() {
189 | layer.removeAllAnimations()
190 | circleOutlineLayer.removeAllAnimations()
191 | insideCircleShapeLayer.removeAllAnimations()
192 | checkmarkShapeLayer.removeAllAnimations()
193 | circleOutlineLayer.transform = CATransform3DIdentity
194 | layer.transform = CATransform3DIdentity
195 | }
196 |
197 | }
198 |
199 | let view = UIView(frame: CGRect(origin: CGPoint.zero,
200 | size: CGSize(width: 300, height: 300)))
201 | let progress = MaterialCircularProgress(frame: CGRect(origin: CGPoint.zero,
202 | size: CGSize(width: 80, height: 80)))
203 | progress.center = CGPoint(x: view.bounds.width * 0.5, y: view.bounds.height * 0.5)
204 | view.addSubview(progress)
205 | PlaygroundPage.current.liveView = view
206 | progress.startAnimating(duration: 3.0)
207 |
--------------------------------------------------------------------------------
/sample.playground/contents.xcplayground:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/sample.playground/playground.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/sample.playground/timeline.xctimeline:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------