├── 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 | ![preview](https://github.com/Tueno/MaterialCircularProgress/blob/master/sample.gif?raw=true) 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 | --------------------------------------------------------------------------------