Swift: How to draw a circle step by step? - swift

I build a project to draw an arc initially, this arc is 1/8 of a circle. Then i put a button on the viewController, whenever I click this button, it will draw another 1/8 of a circle seamless on it .
But I got a problem: when i click the button, it almost draws the arc rapidly(0.25s), not the duration i set before(1s). How to reach that no matter when I click the button, it consumes the same time as the duration i set before?
import UIKit
let π = CGFloat(M_PI)
class ViewController: UIViewController {
var i: CGFloat = 1
var maxStep: CGFloat = 8
var circleLayer: CAShapeLayer?
override func viewDidLoad() {
super.viewDidLoad()
let startAngle = CGFloat(0)
let endAngle = 2*π
let ovalRect = CGRectMake(100, 100, 100, 100)
let ovalPath = UIBezierPath(arcCenter: CGPointMake(CGRectGetMidX(ovalRect), CGRectGetMidY(ovalRect)), radius: CGRectGetWidth(ovalRect), startAngle: startAngle, endAngle: endAngle, clockwise: true)
let circleLayer = CAShapeLayer()
circleLayer.path = ovalPath.CGPath
circleLayer.strokeColor = UIColor.blueColor().CGColor
circleLayer.fillColor = UIColor.clearColor().CGColor
circleLayer.lineWidth = 10.0
circleLayer.lineCap = kCALineCapRound
self.circleLayer = circleLayer
self.view.layer.addSublayer(self.circleLayer)
let anim = CABasicAnimation(keyPath: "strokeEnd")
// here i set the duration
anim.duration = 1.0
anim.fromValue = 0.0
anim.toValue = self.i/self.maxStep
self.circleLayer!.strokeStart = 0.0
self.circleLayer!.strokeEnd = self.i/self.maxStep
self.circleLayer!.addAnimation(anim, forKey: "arc animation")
self.i++
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// when this action be triggered, it almost draws the arc rapidly, why?
#IBAction func buttonAnimate(sender: UIButton) {
if self.i<=self.maxStep {
self.circleLayer!.strokeStart = 0.0
self.circleLayer!.strokeEnd = self.i/self.maxStep
self.i++
}
}
}

Get your animation and reset properties. See code below:
#IBAction func buttonAnimate(sender: UIButton) {
if self.i<=self.maxStep {
self.circleLayer!.strokeStart = 0.0
self.circleLayer!.strokeEnd = self.i/self.maxStep
self.circleLayer?.animationKeys()
let anim = self.circleLayer?.animationForKey("arc animation") as? CABasicAnimation
if anim == nil {
let anim = CABasicAnimation(keyPath: "strokeEnd")
// here i set the duration
anim.duration = 1
anim.fromValue = (self.i - 1)/self.maxStep
anim.toValue = self.i/self.maxStep
self.circleLayer!.addAnimation(anim, forKey: "arc animation")
}
self.i++
}
}
Hope this helps!

Here is the correct solution:
import UIKit
import UIKit
let π = CGFloat(M_PI)
class ViewController: UIViewController {
var i: CGFloat = 1
var maxStep: CGFloat = 8
var prevValue : CGFloat = 0.0
var circleLayer: CAShapeLayer?
override func viewDidLoad() {
super.viewDidLoad()
let startAngle = CGFloat(0)
let endAngle = 2*π
let ovalRect = CGRectMake(100, 100, 100, 100)
let ovalPath = UIBezierPath(arcCenter: CGPointMake(CGRectGetMidX(ovalRect), CGRectGetMidY(ovalRect)), radius: CGRectGetWidth(ovalRect), startAngle: startAngle, endAngle: endAngle, clockwise: true)
let circleLayer = CAShapeLayer()
circleLayer.path = ovalPath.CGPath
circleLayer.strokeColor = UIColor.blueColor().CGColor
circleLayer.fillColor = nil //UIColor.clearColor().CGColor
circleLayer.lineWidth = 10.0
circleLayer.lineCap = kCALineCapRound
self.circleLayer = circleLayer
self.view.layer.addSublayer(self.circleLayer!)
self.circleLayer!.strokeStart = 0.0
//Initial stroke-
setStrokeEndForLayer(self.circleLayer!, from: 0.0, to: self.i / self.maxStep, animated: true)
self.i++
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func buttonAnimate(sender: UIButton) {
if self.i <= self.maxStep {
setStrokeEndForLayer(self.circleLayer!, from: (self.i - 1 ) / self.maxStep, to: self.i / self.maxStep, animated: true)
self.i++
}
}
func setStrokeEndForLayer(layer: CALayer, var from:CGFloat, to: CGFloat, animated: Bool)
{
self.circleLayer!.strokeEnd = to
if animated
{
//Check if there is any existing animation is in progress, if so override, the from value
if let circlePresentationLayer = self.circleLayer!.presentationLayer()
{
from = circlePresentationLayer.strokeEnd
}
//Remove any on going animation
if (self.circleLayer?.animationForKey("arc animation") as? CABasicAnimation != nil)
{
//Remove the current animation
self.circleLayer!.removeAnimationForKey("arc animation")
}
let anim = CABasicAnimation(keyPath: "strokeEnd")
// here i set the duration
anim.duration = 1
anim.fromValue = from
anim.toValue = to
self.circleLayer!.addAnimation(anim, forKey: "arc animation")
}
}
}

Related

Circle Progress Bar for Swift Count down

Inside the circle progress bar there is a constantly changing number (eg 250, 300, 1000). Whenever I click, the number will decrease and circle progress bar will move. I did it with the time counter. But I want to do it with my control. So when I click the button it will move, if I don't click it won't move.
My code :
import UIKit
class ViewController: UIViewController {
let shapeLAyer = CAShapeLayer()
let progressLayer = CAShapeLayer()
override func viewDidLoad() {
super.viewDidLoad()
let center = view.center
let circularPath = UIBezierPath(arcCenter: center, radius: 100, startAngle: -CGFloat.pi/2, endAngle: 2*CGFloat.pi, clockwise: true)
progressLayer.path = circularPath.cgPath
// ui edits
progressLayer.strokeColor = UIColor.black.cgColor
progressLayer.fillColor = UIColor.clear.cgColor
//progressLayer.fillColor = UIColor.clear.cgColor
progressLayer.lineCap = .round
progressLayer.lineWidth = 20.0
view.layer.addSublayer(progressLayer)
shapeLAyer.path = circularPath.cgPath
shapeLAyer.fillColor = UIColor.clear.cgColor
shapeLAyer.strokeColor = UIColor.red.cgColor
shapeLAyer.lineWidth = 10
shapeLAyer.lineCap = .round
shapeLAyer.strokeEnd = 0
view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handle)))
view.layer.addSublayer(shapeLAyer)
}
#objc func handle(){
let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
basicAnimation.toValue = 1
basicAnimation.duration = 3
shapeLAyer.add(basicAnimation, forKey: "urSoBasic")
}
}
[![enter image description here][1]][1]
You can achieve this type of progress by setting proper from value, to value and with the mode of CABasicAnimation
Here, I take two var one for total number (for total progress) and one for current progress number.
From these two var, I converted this value to between 0-1.
First, in your code UIBezierPath angle is wrong.
Replace this line by
let circularPath = UIBezierPath(arcCenter: center, radius: 100, startAngle: -CGFloat.pi/2, endAngle: 3*CGFloat.pi / 2, clockwise: true)
class ViewController: UIViewController {
// Other code
private let totalNumber: CGFloat = 300.0
private var currentProgressNumber: CGFloat = 0
private let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
// viewDidLoad code
func playAnimation(){
if currentProgressNumber > totalNumber || currentProgressNumber < 0 {
return
}
basicAnimation.fromValue = basicAnimation.toValue
basicAnimation.toValue = currentProgressNumber/totalNumber
basicAnimation.isRemovedOnCompletion = false
basicAnimation.duration = 3
basicAnimation.fillMode = CAMediaTimingFillMode.both
shapeLAyer.add(basicAnimation, forKey: "urSoBasic")
}
#IBAction func onIncrement(_ sender: UIButton) {
currentProgressNumber += 15 //<-- Change your increment interval here
playAnimation()
}
#IBAction func onDecrement(_ sender: UIButton) {
currentProgressNumber -= 15 //<-- Change your increment interval here
playAnimation()
}
}
Note: Replace your animation function with playAnimation() and also change the increment-decrement value as per your requirement for testing I used 15.

Swift: Animate object along animating path

I would like to animate little red dot rotating around circle that is expanding in a pulse manner (go from small to big, then start back from small). It seems that little dot keeps rotating around original shape and does not take into account that circle it's expanding... I have this in code:
// MARK: - Properties
private lazy var containerView = UIView()
let littleCircleRadius: CGFloat = 10
private lazy var littleRedDot: CALayer = {
let layer = CALayer()
layer.backgroundColor = UIColor.red.cgColor
let littleDotSize = CGSize(width: 10, height: 10)
layer.frame = CGRect(x: containerView.bounds.center.x - littleDotSize.width / 2,
y: containerView.bounds.center.y - littleCircleRadius - littleDotSize.width/2 ,
width: littleDotSize.width,
height: littleDotSize.height)
return layer
}()
private lazy var littleCircleLayer: CAShapeLayer = {
let layer = CAShapeLayer()
layer.lineWidth = 1.5
layer.lineCap = .round
layer.strokeColor = UIColor.black.cgColor
layer.fillColor = UIColor.clear.cgColor
return layer
}()
// MARK: - Setup
func setup() {
view.addSubview(containerView)
containerView.frame = CGRect(x: 40, y: 200, width: 300, height: 300)
containerView.backgroundColor = UIColor.gray.withAlphaComponent(0.2)
littleCircleLayer.path = makeArcPath(arcCenter: containerView.bounds.center, radius: 10)
containerView.layer.addSublayer(littleCircleLayer)
containerView.layer.addSublayer(littleRedDot)
}
// MARK: - Animations
func animate() {
CATransaction.begin()
CATransaction.setAnimationDuration(1.5)
animateLittleRedDotRotation()
animateCircleExpanding()
CATransaction.commit()
}
func animateLittleRedDotRotation() {
let anim = CAKeyframeAnimation(keyPath: "position")
anim.duration = 1.5
anim.rotationMode = .rotateAuto
anim.repeatCount = Float.infinity
anim.path = littleCircleLayer.path
littleRedDot.add(anim, forKey: "rotate")
}
func animateCircleExpanding() {
let maxCircle = makeArcPath(arcCenter: containerView.bounds.center, radius: 100)
let circleExpandingAnim = CABasicAnimation(keyPath: "path")
circleExpandingAnim.fromValue = littleCircleLayer.path
circleExpandingAnim.toValue = maxCircle
circleExpandingAnim.repeatCount = Float.infinity
circleExpandingAnim.duration = 1.5
littleCircleLayer.add(circleExpandingAnim, forKey: "pulseCircuitAnimation")
}
This creates following effect:
However I would like to achieve for little dot to be rotating along the expanding circle path (as it animates from small circle to bigger circle), not the original small circle path. Any ideas ?
Using CoreAnimation to animate the position of the red dot based upon the path assumes that the path isn't changing. You could, theoretically, define a spiral path that mirrors the expanding circle. Personally, I'd just use CADisplayLink, a special timer designed optimally for screen refreshes, and retire the CoreAnimation calls entirely. E.g.
func startDisplayLink() {
let displayLink = CADisplayLink(target: self, selector: #selector(handleDisplayLink(_:)))
displayLink.add(to: .main, forMode: .common)
}
#objc func handleDisplayLink(_ displayLink: CADisplayLink) {
let percent = CGFloat(displayLink.timestamp).truncatingRemainder(dividingBy: duration) / duration
let radius = ...
let center = containerView.bounds.center
circleLayer.path = makeArcPath(arcCenter: center, radius: radius)
let angle = percent * .pi * 2
let dotCenter = CGPoint(x: center.x + cos(angle) * radius, y: center.y + sin(angle) * radius)
redDot.path = makeArcPath(arcCenter: dotCenter, radius: 5)
}
That yields:
The full example:
class ViewController: UIViewController {
private let radiusRange: ClosedRange<CGFloat> = 10...100
private let duration: CGFloat = 1.5
private lazy var containerView: UIView = {
let containerView = UIView()
containerView.translatesAutoresizingMaskIntoConstraints = false
return containerView
}()
private lazy var redDot: CAShapeLayer = {
let layer = CAShapeLayer()
layer.fillColor = UIColor.red.cgColor
return layer
}()
private lazy var circleLayer: CAShapeLayer = {
let layer = CAShapeLayer()
layer.lineWidth = 1.5
layer.strokeColor = UIColor.black.cgColor
layer.fillColor = UIColor.clear.cgColor
return layer
}()
private weak var displayLink: CADisplayLink?
override func viewDidLoad() {
super.viewDidLoad()
setup()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
startDisplayLink()
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
stopDisplayLink()
}
}
// MARK: Private utility methods
private extension ViewController {
func setup() {
addContainer()
containerView.layer.addSublayer(circleLayer)
containerView.layer.addSublayer(redDot)
}
func addContainer() {
view.addSubview(containerView)
NSLayoutConstraint.activate([
containerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
containerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
containerView.topAnchor.constraint(equalTo: view.topAnchor),
containerView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
func makeArcPath(arcCenter: CGPoint, radius: CGFloat) -> CGPath {
UIBezierPath(arcCenter: arcCenter, radius: radius, startAngle: 0, endAngle: .pi * 2, clockwise: true).cgPath
}
}
// MARK: - DisplayLink related methods
private extension ViewController {
func startDisplayLink() {
stopDisplayLink() // stop existing display link, if any
let displayLink = CADisplayLink(target: self, selector: #selector(handleDisplayLink(_:)))
displayLink.add(to: .main, forMode: .common)
self.displayLink = displayLink
}
func stopDisplayLink() {
displayLink?.invalidate()
}
#objc func handleDisplayLink(_ displayLink: CADisplayLink) {
let percent = CGFloat(displayLink.timestamp).truncatingRemainder(dividingBy: duration) / duration
let radius = radiusRange.percent(percent)
let center = containerView.bounds.center
circleLayer.path = makeArcPath(arcCenter: center, radius: radius)
let angle = percent * .pi * 2
let dotCenter = CGPoint(x: center.x + cos(angle) * radius, y: center.y + sin(angle) * radius)
redDot.path = makeArcPath(arcCenter: dotCenter, radius: 5)
}
}
// MARK: - CGRect extension
extension CGRect {
var center: CGPoint { return CGPoint(x: midX, y: midY) }
}
// MARK: - ClosedRange extension
extension ClosedRange where Bound: FloatingPoint {
func percent(_ percent: Bound) -> Bound {
(upperBound - lowerBound) * percent + lowerBound
}
}

Stop old animation and start new one, once I change segments, swift

Is there a method or function that can stop previous animation, when I switch segment controls?
If I don't remove animation, image that was fading on first segment, starts rotating on the second and then moves on the third segment.
While I switch segments, it works more like appending different animations all the time.
I tried:
- removeAllAnimations(), but it doesn't work properly.
- self.view.layer.removeAnimation(forKey: "moveAnimation"). Tried that one at the end of the code for each segment choice.
Maybe I am not putting it in the right spot, but I can't get it working properly
Here is my code:
import UIKit
class MoveViewController: UIViewController {
#IBOutlet var sgAction : UISegmentedControl!
var moveLayer : CALayer?
#IBAction func segmentDidChange(sender : UISegmentedControl){
updateAction()
}
func updateAction(){
let action = sgAction.selectedSegmentIndex
if action == 0 {
//fade
let fadeAnimation = CABasicAnimation(keyPath: "opacity")
self.view.layer.removeAnimation(forKey: "fadeAnimation")
fadeAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
fadeAnimation.fromValue = NSNumber.init(value: 1.0)
fadeAnimation.toValue = NSNumber.init(value: 0.0)
fadeAnimation.isRemovedOnCompletion = false
fadeAnimation.duration = 3.0
fadeAnimation.beginTime = 1.0
fadeAnimation.isAdditive = false
fadeAnimation.fillMode = CAMediaTimingFillMode.both
fadeAnimation.repeatCount = Float.infinity
moveLayer?.add(fadeAnimation, forKey: nil)
} else if action == 1 {
//2. rotate
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
self.view.layer.removeAnimation(forKey: "rotateAnimation")
rotateAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
rotateAnimation.fromValue = 0
rotateAnimation.toValue = 2 * Double.pi
rotateAnimation.isRemovedOnCompletion = false
rotateAnimation.duration = 1.0
rotateAnimation.repeatCount = Float.infinity
moveLayer?.add(rotateAnimation, forKey:nil)
} else {
//3. move
let moveAnimation = CABasicAnimation(keyPath: "position")
self.view.layer.removeAnimation(forKey: "moveAnimation")
moveAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
moveAnimation.fromValue = NSValue.init(cgPoint: CGPoint(x: 0, y: 0))
moveAnimation.toValue = NSValue.init(cgPoint: CGPoint(x: 700, y: 500))
moveAnimation.isRemovedOnCompletion = false
moveAnimation.duration = 3.0
moveAnimation.repeatCount = Float.infinity
moveLayer?.add(moveAnimation, forKey: nil)
}
}
// moveLayer?.removeAllAnimations()
func createImg(){
let displayImage = UIImage(named: "balloon.png")
moveLayer = CALayer.init()
moveLayer?.contents = displayImage?.cgImage
moveLayer?.bounds = CGRect(x: 0, y: 0, width: 150, height: 150)
moveLayer?.position = CGPoint(x: 300, y: 200)
self.view.layer.addSublayer(moveLayer!)
}
override func viewDidLoad() {
super.viewDidLoad()
createImg()
// moveLayer?.removeAllAnimations()
updateAction()
}
}
When you add moveLayer?.removeAllAnimations() and self.moveLayer.layoutIfNeed() to the segmentDidChange(sender : UISegmentedControl). Also the other key item that needs to get changed is moveLayer?.add(fadeAnimation, forKey: "nil") to moveLayer?.add(fadeAnimation, forKey: "fadeAnimation").
import UIKit
class MoveViewController: UIViewController {
#IBOutlet var sgAction : UISegmentedControl!
var moveLayer : CALayer?
override func viewDidLoad() {
super.viewDidLoad()
createImg()
updateAction()
}
#IBAction func segmentDidChange(sender : UISegmentedControl){
moveLayer?.removeAllAnimations()
self.moveLayer?.layoutIfNeeded()
updateAction()
}
func updateAction(){
switch sgAction.selectedSegmentIndex {
case 0:
//fade
let fadeAnimation = CABasicAnimation(keyPath: "opacity")
fadeAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
fadeAnimation.fromValue = NSNumber.init(value: 1.0)
fadeAnimation.toValue = NSNumber.init(value: 0.0)
fadeAnimation.isRemovedOnCompletion = false
fadeAnimation.duration = 3.0
fadeAnimation.beginTime = 1.0
fadeAnimation.isAdditive = false
fadeAnimation.fillMode = CAMediaTimingFillMode.both
fadeAnimation.repeatCount = Float.infinity
moveLayer?.add(fadeAnimation, forKey: "fadeAnimation")
case 1:
//2. rotate
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotateAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
rotateAnimation.fromValue = 0
rotateAnimation.toValue = 2 * Double.pi
rotateAnimation.isRemovedOnCompletion = false
rotateAnimation.duration = 1.0
rotateAnimation.repeatCount = Float.infinity
moveLayer?.add(rotateAnimation, forKey: "rotateAnimation")
case 2:
//3. move
let moveAnimation = CABasicAnimation(keyPath: "position")
moveAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
moveAnimation.fromValue = NSValue.init(cgPoint: CGPoint(x: 0, y: 0))
moveAnimation.toValue = NSValue.init(cgPoint: CGPoint(x: 700, y: 500))
moveAnimation.isRemovedOnCompletion = false
moveAnimation.duration = 3.0
moveAnimation.repeatCount = Float.infinity
moveLayer?.add(moveAnimation, forKey: "position")
default: break
}
}
func createImg(){
let displayImage = UIImage(named: "balloon.png")
moveLayer = CALayer.init()
moveLayer?.contents = displayImage?.cgImage
moveLayer?.bounds = CGRect(x: 0, y: 0, width: 150, height: 150)
moveLayer?.position = CGPoint(x: 300, y: 200)
self.view.layer.addSublayer(moveLayer!)
}
}

Change color gradient around donut view

I am trying to make an animated donut view that when given a value between 0 and 100 it will animate round the view up to that number. I have this working fine but want to fade the color from one to another, then another on the way around. Currently, when I add my gradient it goes from left to right and not around the circumference of the donut view.
class CircleScoreView: UIView {
private let outerCircleLayer = CAShapeLayer()
private let outerCircleGradientLayer = CAGradientLayer()
private let outerCircleLineWidth: CGFloat = 5
override init(frame: CGRect) {
super.init(frame: .zero)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
buildLayers()
}
/// Value must be within 0...100 range
func setScore(_ value: Int, animated: Bool = false) {
if value != 0 {
let clampedValue: CGFloat = CGFloat(value.clamped(to: 0...100)) / 100
if !animated {
outerCircleLayer.strokeEnd = clampedValue
} else {
let outerCircleAnimation = CABasicAnimation(keyPath: "strokeEnd")
outerCircleAnimation.duration = 1.0
outerCircleAnimation.fromValue = 0
outerCircleAnimation.toValue = clampedValue
outerCircleAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
outerCircleLayer.strokeEnd = clampedValue
outerCircleLayer.add(outerCircleAnimation, forKey: "outerCircleAnimation")
}
outerCircleGradientLayer.colors = [Constant.Palette.CircleScoreView.startValue.cgColor,
Constant.Palette.CircleScoreView.middleValue.cgColor,
Constant.Palette.CircleScoreView.endValue.cgColor]
}
}
private func buildLayers() {
// Outer background circle
let arcCenter = CGPoint(x: frame.size.width / 2, y: frame.size.height / 2)
let startAngle = CGFloat(-0.5 * Double.pi)
let endAngle = CGFloat(1.5 * Double.pi)
let circlePath = UIBezierPath(arcCenter: arcCenter,
radius: (frame.size.width - outerCircleLineWidth) / 2,
startAngle: startAngle,
endAngle: endAngle,
clockwise: true)
// Outer circle
setupOuterCircle(outerCirclePath: circlePath)
}
private func setupOuterCircle(outerCirclePath: UIBezierPath) {
outerCircleLayer.path = outerCirclePath.cgPath
outerCircleLayer.fillColor = UIColor.clear.cgColor
outerCircleLayer.strokeColor = UIColor.black.cgColor
outerCircleLayer.lineWidth = outerCircleLineWidth
outerCircleLayer.lineCap = CAShapeLayerLineCap.round
outerCircleGradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
outerCircleGradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
outerCircleGradientLayer.frame = bounds
outerCircleGradientLayer.mask = outerCircleLayer
layer.addSublayer(outerCircleGradientLayer)
}
}
I am going for something like this but the color isn't one block but gradients around the donut view from one color to the next.
If you imported AngleGradientLayer into your project then all you should need to do is change:
private let outerCircleGradientLayer = CAGradientLayer() to
private let outerCircleGradientLayer = AngleGradientLayer()

Animate drawing of a circle

I'm looking for a way to animate the drawing of a circle. I have been able to create the circle, but it draws it all together.
Here is my CircleView class:
import UIKit
class CircleView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.clearColor()
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func drawRect(rect: CGRect) {
// Get the Graphics Context
var context = UIGraphicsGetCurrentContext();
// Set the circle outerline-width
CGContextSetLineWidth(context, 5.0);
// Set the circle outerline-colour
UIColor.redColor().set()
// Create Circle
CGContextAddArc(context, (frame.size.width)/2, frame.size.height/2, (frame.size.width - 10)/2, 0.0, CGFloat(M_PI * 2.0), 1)
// Draw
CGContextStrokePath(context);
}
}
And here is how I add it to the view hierarchy in my view controller:
func addCircleView() {
let diceRoll = CGFloat(Int(arc4random_uniform(7))*50)
var circleWidth = CGFloat(200)
var circleHeight = circleWidth
// Create a new CircleView
var circleView = CircleView(frame: CGRectMake(diceRoll, 0, circleWidth, circleHeight))
view.addSubview(circleView)
}
Is there a way to animate the drawing of the circle over 1 second?
Example, part way through the animation it would look something like the blue line in this image:
The easiest way to do this is to use the power of core animation to do most of the work for you. To do that, we'll have to move your circle drawing code from your drawRect function to a CAShapeLayer. Then, we can use a CABasicAnimation to animate CAShapeLayer's strokeEnd property from 0.0 to 1.0. strokeEnd is a big part of the magic here; from the docs:
Combined with the strokeStart property, this property defines the
subregion of the path to stroke. The value in this property indicates
the relative point along the path at which to finish stroking while
the strokeStart property defines the starting point. A value of 0.0
represents the beginning of the path while a value of 1.0 represents
the end of the path. Values in between are interpreted linearly along
the path length.
If we set strokeEnd to 0.0, it won't draw anything. If we set it to 1.0, it'll draw a full circle. If we set it to 0.5, it'll draw a half circle. etc.
So, to start, lets create a CAShapeLayer in your CircleView's init function and add that layer to the view's sublayers (also be sure to remove the drawRect function since the layer will be drawing the circle now):
let circleLayer: CAShapeLayer!
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.clearColor()
// Use UIBezierPath as an easy way to create the CGPath for the layer.
// The path should be the entire circle.
let circlePath = UIBezierPath(arcCenter: CGPoint(x: frame.size.width / 2.0, y: frame.size.height / 2.0), radius: (frame.size.width - 10)/2, startAngle: 0.0, endAngle: CGFloat(Double.pi * 2.0), clockwise: true)
// Setup the CAShapeLayer with the path, colors, and line width
circleLayer = CAShapeLayer()
circleLayer.path = circlePath.CGPath
circleLayer.fillColor = UIColor.clearColor().CGColor
circleLayer.strokeColor = UIColor.redColor().CGColor
circleLayer.lineWidth = 5.0;
// Don't draw the circle initially
circleLayer.strokeEnd = 0.0
// Add the circleLayer to the view's layer's sublayers
layer.addSublayer(circleLayer)
}
Note: We're setting circleLayer.strokeEnd = 0.0 so that the circle isn't drawn right away.
Now, lets add a function that we can call to trigger the circle animation:
func animateCircle(duration: NSTimeInterval) {
// We want to animate the strokeEnd property of the circleLayer
let animation = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.strokeEnd))
// Set the animation duration appropriately
animation.duration = duration
// Animate from 0 (no circle) to 1 (full circle)
animation.fromValue = 0
animation.toValue = 1
// Do a linear animation (i.e. the speed of the animation stays the same)
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
// Set the circleLayer's strokeEnd property to 1.0 now so that it's the
// right value when the animation ends.
circleLayer.strokeEnd = 1.0
// Do the actual animation
circleLayer.add(animation, forKey: "animateCircle")
}
Then, all we need to do is change your addCircleView function so that it triggers the animation when you add the CircleView to its superview:
func addCircleView() {
let diceRoll = CGFloat(Int(arc4random_uniform(7))*50)
var circleWidth = CGFloat(200)
var circleHeight = circleWidth
// Create a new CircleView
var circleView = CircleView(frame: CGRectMake(diceRoll, 0, circleWidth, circleHeight))
view.addSubview(circleView)
// Animate the drawing of the circle over the course of 1 second
circleView.animateCircle(1.0)
}
All that put together should look something like this:
Note: It won't repeat like that, it'll stay a full circle after it animates.
Mikes answer updated for Swift 3.0
var circleLayer: CAShapeLayer!
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.clear
// Use UIBezierPath as an easy way to create the CGPath for the layer.
// The path should be the entire circle.
let circlePath = UIBezierPath(arcCenter: CGPoint(x: frame.size.width / 2.0, y: frame.size.height / 2.0), radius: (frame.size.width - 10)/2, startAngle: 0.0, endAngle: CGFloat(M_PI * 2.0), clockwise: true)
// Setup the CAShapeLayer with the path, colors, and line width
circleLayer = CAShapeLayer()
circleLayer.path = circlePath.cgPath
circleLayer.fillColor = UIColor.clear.cgColor
circleLayer.strokeColor = UIColor.red.cgColor
circleLayer.lineWidth = 5.0;
// Don't draw the circle initially
circleLayer.strokeEnd = 0.0
// Add the circleLayer to the view's layer's sublayers
layer.addSublayer(circleLayer)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func animateCircle(duration: TimeInterval) {
// We want to animate the strokeEnd property of the circleLayer
let animation = CABasicAnimation(keyPath: "strokeEnd")
// Set the animation duration appropriately
animation.duration = duration
// Animate from 0 (no circle) to 1 (full circle)
animation.fromValue = 0
animation.toValue = 1
// Do a linear animation (i.e The speed of the animation stays the same)
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
// Set the circleLayer's strokeEnd property to 1.0 now so that it's the
// Right value when the animation ends
circleLayer.strokeEnd = 1.0
// Do the actual animation
circleLayer.add(animation, forKey: "animateCircle")
}
To call the function:
func addCircleView() {
let diceRoll = CGFloat(Int(arc4random_uniform(7))*50)
var circleWidth = CGFloat(200)
var circleHeight = circleWidth
// Create a new CircleView
let circleView = CircleView(frame: CGRect(x: diceRoll, y: 0, width: circleWidth, height: circleHeight))
//let test = CircleView(frame: CGRect(x: diceRoll, y: 0, width: circleWidth, height: circleHeight))
view.addSubview(circleView)
// Animate the drawing of the circle over the course of 1 second
circleView.animateCircle(duration: 1.0)
}
Mike's answer is great! Another nice and simple way to do it is use drawRect combined with setNeedsDisplay(). It seems laggy, but its not :-)
We want to draw a circle starting from the top, which is -90° and ends at 270°. The circle's center is (centerX, centerY), with a given radius. CurrentAngle is the current angle of the end-point of the circle, going from minAngle (-90) to maxAngle (270).
// MARK: Properties
let centerX:CGFloat = 55
let centerY:CGFloat = 55
let radius:CGFloat = 50
var currentAngle:Float = -90
let minAngle:Float = -90
let maxAngle:Float = 270
In drawRect, we specify how the circle is supposed to display :
override func drawRect(rect: CGRect) {
let context = UIGraphicsGetCurrentContext()
let path = CGPathCreateMutable()
CGPathAddArc(path, nil, centerX, centerY, radius, CGFloat(GLKMathDegreesToRadians(minAngle)), CGFloat(GLKMathDegreesToRadians(currentAngle)), false)
CGContextAddPath(context, path)
CGContextSetStrokeColorWithColor(context, UIColor.blueColor().CGColor)
CGContextSetLineWidth(context, 3)
CGContextStrokePath(context)
}
The problem is that right now, as currentAngle is not changing, the circle is static, and doesn't even show, as currentAngle = minAngle.
We then create a timer, and whenever that timer fires, we increase currentAngle. At the top of your class, add the timing between two fires :
let timeBetweenDraw:CFTimeInterval = 0.01
In your init, add the timer :
NSTimer.scheduledTimerWithTimeInterval(timeBetweenDraw, target: self, selector: #selector(updateTimer), userInfo: nil, repeats: true)
We can add the function that will be called when the timer fires :
func updateTimer() {
if currentAngle < maxAngle {
currentAngle += 1
}
}
Sadly, when running the app, nothing displays because we did not specify the system that it should draw again. This is done by calling setNeedsDisplay(). Here is the updated timer function :
func updateTimer() {
if currentAngle < maxAngle {
currentAngle += 1
setNeedsDisplay()
}
}
_
_
_
All the code you need is summed-up here :
import UIKit
import GLKit
class CircleClosing: UIView {
// MARK: Properties
let centerX:CGFloat = 55
let centerY:CGFloat = 55
let radius:CGFloat = 50
var currentAngle:Float = -90
let timeBetweenDraw:CFTimeInterval = 0.01
// MARK: Init
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
func setup() {
self.backgroundColor = UIColor.clearColor()
NSTimer.scheduledTimerWithTimeInterval(timeBetweenDraw, target: self, selector: #selector(updateTimer), userInfo: nil, repeats: true)
}
// MARK: Drawing
func updateTimer() {
if currentAngle < 270 {
currentAngle += 1
setNeedsDisplay()
}
}
override func drawRect(rect: CGRect) {
let context = UIGraphicsGetCurrentContext()
let path = CGPathCreateMutable()
CGPathAddArc(path, nil, centerX, centerY, radius, -CGFloat(M_PI/2), CGFloat(GLKMathDegreesToRadians(currentAngle)), false)
CGContextAddPath(context, path)
CGContextSetStrokeColorWithColor(context, UIColor.blueColor().CGColor)
CGContextSetLineWidth(context, 3)
CGContextStrokePath(context)
}
}
If you want to change the speed, just modify the updateTimer function, or the rate at which this function is called. Also, you might want to invalidate the timer once the circle is complete, which I forgot to do :-)
NB: To add the circle in your storyboard, just add a view, select it, go to its Identity Inspector, and as Class, specify CircleClosing.
Cheers! bRo
If you want a completion handler, this is another solution similar to the one by Mike S, done in Swift 3.0
func animateCircleFull(duration: TimeInterval) {
CATransaction.begin()
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.duration = duration
animation.fromValue = 0
animation.toValue = 1
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
circleLayer.strokeEnd = 1.0
CATransaction.setCompletionBlock {
print("animation complete")
}
// Do the actual animation
circleLayer.add(animation, forKey: "animateCircle")
CATransaction.commit()
}
With the completion handler, you can run the animation again either by recursively calling the same function to do the animation over again (which won't look very nice), or you can have a reversed function that will continuously chain until a condition is met, for example:
func animate(duration: TimeInterval){
self.isAnimating = true
self.animateCircleFull(duration: 1)
}
func endAnimate(){
self.isAnimating = false
}
func animateCircleFull(duration: TimeInterval) {
if self.isAnimating{
CATransaction.begin()
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.duration = duration
animation.fromValue = 0
animation.toValue = 1
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
circleLayer.strokeEnd = 1.0
CATransaction.setCompletionBlock {
self.animateCircleEmpty(duration: duration)
}
// Do the actual animation
circleLayer.add(animation, forKey: "animateCircle")
CATransaction.commit()
}
}
func animateCircleEmpty(duration: TimeInterval){
if self.isAnimating{
CATransaction.begin()
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.duration = duration
animation.fromValue = 1
animation.toValue = 0
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
circleLayer.strokeEnd = 0
CATransaction.setCompletionBlock {
self.animateCircleFull(duration: duration)
}
// Do the actual animation
circleLayer.add(animation, forKey: "animateCircle")
CATransaction.commit()
}
}
To make it even fancier, you can change the direction of the animation like this:
func setCircleClockwise(){
let circlePath = UIBezierPath(arcCenter: CGPoint(x: frame.size.width / 2.0, y: frame.size.height / 2.0), radius: (frame.size.width - 10)/2, startAngle: 0.0, endAngle: CGFloat(M_PI * 2.0), clockwise: true)
self.circleLayer.removeFromSuperlayer()
self.circleLayer = formatCirle(circlePath: circlePath)
self.layer.addSublayer(self.circleLayer)
}
func setCircleCounterClockwise(){
let circlePath = UIBezierPath(arcCenter: CGPoint(x: frame.size.width / 2.0, y: frame.size.height / 2.0), radius: (frame.size.width - 10)/2, startAngle: 0.0, endAngle: CGFloat(M_PI * 2.0), clockwise: false)
self.circleLayer.removeFromSuperlayer()
self.circleLayer = formatCirle(circlePath: circlePath)
self.layer.addSublayer(self.circleLayer)
}
func formatCirle(circlePath: UIBezierPath) -> CAShapeLayer{
let circleShape = CAShapeLayer()
circleShape.path = circlePath.cgPath
circleShape.fillColor = UIColor.clear.cgColor
circleShape.strokeColor = UIColor.red.cgColor
circleShape.lineWidth = 10.0;
circleShape.strokeEnd = 0.0
return circleShape
}
func animate(duration: TimeInterval){
self.isAnimating = true
self.animateCircleFull(duration: 1)
}
func endAnimate(){
self.isAnimating = false
}
func animateCircleFull(duration: TimeInterval) {
if self.isAnimating{
CATransaction.begin()
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.duration = duration
animation.fromValue = 0
animation.toValue = 1
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
circleLayer.strokeEnd = 1.0
CATransaction.setCompletionBlock {
self.setCircleCounterClockwise()
self.animateCircleEmpty(duration: duration)
}
// Do the actual animation
circleLayer.add(animation, forKey: "animateCircle")
CATransaction.commit()
}
}
func animateCircleEmpty(duration: TimeInterval){
if self.isAnimating{
CATransaction.begin()
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.duration = duration
animation.fromValue = 1
animation.toValue = 0
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
circleLayer.strokeEnd = 0
CATransaction.setCompletionBlock {
self.setCircleClockwise()
self.animateCircleFull(duration: duration)
}
// Do the actual animation
circleLayer.add(animation, forKey: "animateCircle")
CATransaction.commit()
}
}
updating #Mike S's answer for Swift 5
works for frame manually、 storyboard setup、 autolayout setup
class CircleView: UIView {
let circleLayer: CAShapeLayer = {
// Setup the CAShapeLayer with the path, colors, and line width
let circle = CAShapeLayer()
circle.fillColor = UIColor.clear.cgColor
circle.strokeColor = UIColor.red.cgColor
circle.lineWidth = 5.0
// Don't draw the circle initially
circle.strokeEnd = 0.0
return circle
}()
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
func setup(){
backgroundColor = UIColor.clear
// Add the circleLayer to the view's layer's sublayers
layer.addSublayer(circleLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
// Use UIBezierPath as an easy way to create the CGPath for the layer.
// The path should be the entire circle.
let circlePath = UIBezierPath(arcCenter: CGPoint(x: frame.size.width / 2.0, y: frame.size.height / 2.0), radius: (frame.size.width - 10)/2, startAngle: 0.0, endAngle: CGFloat(Double.pi * 2.0), clockwise: true)
circleLayer.path = circlePath.cgPath
}
func animateCircle(duration t: TimeInterval) {
// We want to animate the strokeEnd property of the circleLayer
let animation = CABasicAnimation(keyPath: "strokeEnd")
// Set the animation duration appropriately
animation.duration = t
// Animate from 0 (no circle) to 1 (full circle)
animation.fromValue = 0
animation.toValue = 1
// Do a linear animation (i.e. the speed of the animation stays the same)
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
// Set the circleLayer's strokeEnd property to 1.0 now so that it's the
// right value when the animation ends.
circleLayer.strokeEnd = 1.0
// Do the actual animation
circleLayer.add(animation, forKey: "animateCircle")
}
}
Usage :
sample code for frame manually、 storyboard setup、 autolayout setup
class ViewController: UIViewController {
#IBOutlet weak var circleV: CircleView!
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func animateFrame(_ sender: UIButton) {
let diceRoll = CGFloat(Int(arc4random_uniform(7))*30)
let circleEdge = CGFloat(200)
// Create a new CircleView
let circleView = CircleView(frame: CGRect(x: 50, y: diceRoll, width: circleEdge, height: circleEdge))
view.addSubview(circleView)
// Animate the drawing of the circle over the course of 1 second
circleView.animateCircle(duration: 1.0)
}
#IBAction func animateAutolayout(_ sender: UIButton) {
let circleView = CircleView(frame: CGRect.zero)
circleView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(circleView)
circleView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
circleView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
circleView.widthAnchor.constraint(equalToConstant: 250).isActive = true
circleView.heightAnchor.constraint(equalToConstant: 250).isActive = true
// Animate the drawing of the circle over the course of 1 second
circleView.animateCircle(duration: 1.0)
}
#IBAction func animateStoryboard(_ sender: UIButton) {
// Animate the drawing of the circle over the course of 1 second
circleV.animateCircle(duration: 1.0)
}
}
No only you can subclass an UIView, you can also go slightly deeper, subclass an CALayer
In other words, CoreAnimation's strokeEnd is OK. To call CALayer's draw(in ctx:) frequently is also OK
and the round line cap is nice
The key point is to override CALayer's method action(forKey:)
Actions define dynamic behaviors for a layer. For example, the animatable properties of a layer typically have corresponding action objects to initiate the actual animations. When that property changes, the layer looks for the action object associated with the property name and executes it.
The internal subclass for CAShapeLayer
/**
The internal subclass for CAShapeLayer.
This is the class that handles all the drawing and animation.
This class is not interacted with, instead
properties are set in UICircularRing
*/
class UICircularRingLayer: CAShapeLayer {
// MARK: Properties
#NSManaged var val: CGFloat
let ringWidth: CGFloat = 20
let startAngle = CGFloat(-90).rads
// MARK: Init
override init() {
super.init()
}
override init(layer: Any) {
guard let layer = layer as? UICircularRingLayer else { fatalError("unable to copy layer") }
super.init(layer: layer)
}
required init?(coder aDecoder: NSCoder) { return nil }
// MARK: Draw
/**
Override for custom drawing.
Draws the ring
*/
override func draw(in ctx: CGContext) {
super.draw(in: ctx)
UIGraphicsPushContext(ctx)
// Draw the rings
drawRing(in: ctx)
UIGraphicsPopContext()
}
// MARK: Animation methods
/**
Watches for changes in the val property, and setNeedsDisplay accordingly
*/
override class func needsDisplay(forKey key: String) -> Bool {
if key == "val" {
return true
} else {
return super.needsDisplay(forKey: key)
}
}
/**
Creates animation when val property is changed
*/
override func action(forKey event: String) -> CAAction? {
if event == "val"{
let animation = CABasicAnimation(keyPath: "val")
animation.fromValue = presentation()?.value(forKey: "val")
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
animation.duration = 2
return animation
} else {
return super.action(forKey: event)
}
}
/**
Draws the ring for the view.
Sets path properties according to how the user has decided to customize the view.
*/
private func drawRing(in ctx: CGContext) {
let center: CGPoint = CGPoint(x: bounds.midX, y: bounds.midY)
let radiusIn: CGFloat = (min(bounds.width, bounds.height) - ringWidth)/2
// Start drawing
let innerPath: UIBezierPath = UIBezierPath(arcCenter: center,
radius: radiusIn,
startAngle: startAngle,
endAngle: toEndAngle,
clockwise: true)
// Draw path
ctx.setLineWidth(ringWidth)
ctx.setLineJoin(.round)
ctx.setLineCap(CGLineCap.round)
ctx.setStrokeColor(UIColor.red.cgColor)
ctx.addPath(innerPath.cgPath)
ctx.drawPath(using: .stroke)
}
var toEndAngle: CGFloat {
return (val * 360.0).rads + startAngle
}
}
helper methods
/**
A private extension to CGFloat in order to provide simple
conversion from degrees to radians, used when drawing the rings.
*/
extension CGFloat {
var rads: CGFloat { return self * CGFloat.pi / 180 }
}
use an UIView subclass, with the internal custom CALayer
#IBDesignable open class UICircularRing: UIView {
/**
Set the ring layer to the default layer, casted as custom layer
*/
var ringLayer: UICircularRingLayer {
return layer as! UICircularRingLayer
}
/**
Overrides the default layer with the custom UICircularRingLayer class
*/
override open class var layerClass: AnyClass {
return UICircularRingLayer.self
}
/**
Override public init to setup() the layer and view
*/
override public init(frame: CGRect) {
super.init(frame: frame)
// Call the internal initializer
setup()
}
/**
Override public init to setup() the layer and view
*/
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
// Call the internal initializer
setup()
}
/**
This method initializes the custom CALayer to the default values
*/
func setup(){
// Helps with pixelation and blurriness on retina devices
ringLayer.contentsScale = UIScreen.main.scale
ringLayer.shouldRasterize = true
ringLayer.rasterizationScale = UIScreen.main.scale * 2
ringLayer.masksToBounds = false
backgroundColor = UIColor.clear
ringLayer.backgroundColor = UIColor.clear.cgColor
ringLayer.val = 0
}
func startAnimation() {
ringLayer.val = 1
}
}
Usage:
class ViewController: UIViewController {
let progressRing = UICircularRing(frame: CGRect(x: 100, y: 100, width: 250, height: 250))
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(progressRing)
}
#IBAction func animate(_ sender: UIButton) {
progressRing.startAnimation()
}
}
with an indicator image to set up the angle