iOS: Increase animation by click a button - ios

I have the following animation, I need to implement a code, by clicking on a button the animation increased by one until a value that I want to reach it.
I try the following code:
let strokeIt = CABasicAnimation(keyPath: "strokeEnd")
let timeLeftShapeLayer = CAShapeLayer()
let bgShapeLayer = CAShapeLayer()
override func viewDidLoad() {
drawBgShape()
drawTimeLeftShape()
}
func drawBgShape() {
bgShapeLayer.path = UIBezierPath(arcCenter: CGPoint(x: view.frame.midX , y: view.frame.midY), radius:
100, startAngle: -90.degreesToRadians, endAngle: 270.degreesToRadians, clockwise: true).cgPath
bgShapeLayer.strokeColor = UIColor.white.cgColor
bgShapeLayer.fillColor = UIColor.clear.cgColor
bgShapeLayer.lineWidth = 15
view.layer.addSublayer(bgShapeLayer)
}
func drawTimeLeftShape() {
timeLeftShapeLayer.path = UIBezierPath(arcCenter: CGPoint(x: view.frame.midX , y: view.frame.midY), radius:
100, startAngle: -90.degreesToRadians, endAngle: 270.degreesToRadians, clockwise: true).cgPath
timeLeftShapeLayer.strokeColor = UIColor.red.cgColor
timeLeftShapeLayer.fillColor = UIColor.clear.cgColor
timeLeftShapeLayer.lineWidth = 15
view.layer.addSublayer(timeLeftShapeLayer)
}
and when click a button :
var x = 0.0
#IBAction func click(_ sender: Any) {
strokeIt.fromValue = x
strokeIt.toValue = x + 0.1
strokeIt.duration = 0.25
timeLeftShapeLayer.add(strokeIt, forKey: nil)
x = x + 0.1
}
How can I achieve that?

You don't need to use an explicit animation to achieve this.
For some properties of CALayer, Core Animation will add an implicit animation for you. The strokeEnd property is one of these.
This means that Core Animation will automatically animate any changes to a layer's strokeEnd property. You don't need to create an animation object and add it yourself.
There are a few things you'll need to amend with your implementation to get it working.
1) The background circle, to look like the image you've posted, should have a grey strokeColor within your drawBgShape function:
bgShapeLayer.strokeColor = UIColor.lightGray.cgColor
2) The strokeEnd for timeLeftShapeLayer should be set to 0.0 as its starting value within your drawTimeLeftShape function.
timeLeftShapeLayer.strokeEnd = 0.0
3) Remove the strokeIt animation property from your class, you don't need it.
4) Update your click function to slowly increment the strokeEnd property of timeLeftShapeLayer directly. This is one simple line of code:
timeLeftShapeLayer.strokeEnd += 0.1
Here's an example implementation:
class ViewController: UIViewController {
let timeLeftShapeLayer = CAShapeLayer()
let bgShapeLayer = CAShapeLayer()
override func viewDidLoad() {
super.viewDidLoad()
drawBgShape()
drawTimeLeftShape()
}
func drawBgShape() {
bgShapeLayer.path = UIBezierPath(
arcCenter: CGPoint(x: view.frame.midX , y: view.frame.midY),
radius: 100,
startAngle: -90.degreesToRadians,
endAngle: 270.degreesToRadians,
clockwise: true
).cgPath
bgShapeLayer.strokeColor = UIColor.lightGray.cgColor
bgShapeLayer.fillColor = UIColor.clear.cgColor
bgShapeLayer.lineWidth = 15
view.layer.addSublayer(bgShapeLayer)
}
func drawTimeLeftShape() {
timeLeftShapeLayer.path = UIBezierPath(
arcCenter: CGPoint(x: view.frame.midX , y: view.frame.midY),
radius: 100,
startAngle: -90.degreesToRadians,
endAngle: 270.degreesToRadians,
clockwise: true
).cgPath
timeLeftShapeLayer.strokeColor = UIColor.red.cgColor
timeLeftShapeLayer.fillColor = UIColor.clear.cgColor
timeLeftShapeLayer.lineWidth = 15
timeLeftShapeLayer.strokeEnd = 0.0
view.layer.addSublayer(timeLeftShapeLayer)
}
#IBAction func click(_ sender: Any) {
timeLeftShapeLayer.strokeEnd += 0.1
}
}

Related

CABasicAnimation Stroke End Animation Percentage

I have a custom progress view that animates based on Character Count. (similar to twitter)
However, my calculation seems to be off. I want the stroke layer to circle around based on the percentage.
Here's my code:
class CharacterCountView: UIView {
private let progressLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
setUpView()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
#objc func animateToValue(value: Double) {
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.toValue = value
animation.duration = 0.5
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
progressLayer.add(animation, forKey: "strokeAnimation")
progressLayer.strokeEnd = value
if value > 1 {
print("100% REACHED")
}
}
}
extension CharacterCountView {
fileprivate func setUpView() {
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(animateToValue)))
let trackLayer = CAShapeLayer()
trackLayer.frame = bounds
let path = UIBezierPath(arcCenter: trackLayer.position, radius: 8, startAngle: -CGFloat.pi / 2, endAngle: 2 * CGFloat.pi, clockwise: true)
trackLayer.path = path.cgPath
trackLayer.strokeColor = UIColor.systemGray2.cgColor
trackLayer.lineWidth = 1.5
trackLayer.fillColor = Color.background.cgColor
trackLayer.lineCap = .round
layer.addSublayer(trackLayer)
progressLayer.frame = bounds
progressLayer.path = path.cgPath
progressLayer.strokeColor = Color.accent.cgColor
progressLayer.lineWidth = 1.5
progressLayer.fillColor = UIColor.clear.cgColor
progressLayer.lineCap = .round
progressLayer.strokeEnd = 0
layer.addSublayer(progressLayer)
}
}
In my Text Did Change Function:
func textDidChange(to value: String) {
if value.isEmpty || value.count > 280 {
postBarButtonItem.isEnabled = false
} else {
postBarButtonItem.isEnabled = true
}
let characterCount = Double(value.count)
let percentage = characterCount / 280
print(percentage)
createPostInputAccessoryView.characterCountView.animateToValue(value: percentage)
}
Its topping out at around 70% what is wrong with my calculation?
Your endAngle is wrong...
Let's make it a little more "readable":
let path = UIBezierPath(arcCenter: trackLayer.position,
radius: 8,
startAngle: -CGFloat.pi / 2,
endAngle: 2 * CGFloat.pi,
clockwise: true)
This: startAngle: -CGFloat.pi / 2 is minus 90-degrees, or 12 o'clock on the circle.
But this: endAngle: 2 * CGFloat.pi is 360-degrees, or 3 o'clock
So you have 450-degrees instead of 360-degrees.
I find it helps to use multipliers only, instead of mixing multiplication and division:
let path = UIBezierPath(arcCenter: trackLayer.position,
radius: 8,
startAngle: -CGFloat.pi * 0.5,
endAngle: CGFloat.pi * 1.5,
clockwise: true)
Now we know we're going from minus 90-degrees to 270-degrees ... for 360-degrees total.

Circular timer using CAShapelayer

Tried to create a circular timer for my app end up with this
override func drawRect(rect: CGRect) {
let context = UIGraphicsGetCurrentContext()
let progressWidth: CGFloat = 10;
let centerX = CGRectGetMidX(rect)
let centerY = CGRectGetMidY(rect)
let center: CGPoint = CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect))
let radius: CGFloat = rect.width / 2
var circlePath = UIBezierPath(arcCenter: center, radius: radius, startAngle: CGFloat(0), endAngle:CGFloat(M_PI * 2), clockwise: true)
let backgroundCircle = CAShapeLayer()
backgroundCircle.path = circlePath.CGPath
backgroundCircle.fillColor = backgroundCircleFillColor
self.layer.addSublayer(backgroundCircle)
circlePath = UIBezierPath(arcCenter: center, radius: radius-progressWidth/2, startAngle: -CGFloat(GLKMathDegreesToRadians(90)), endAngle:CGFloat(GLKMathDegreesToRadians(currentAngle)), clockwise: true)
let progressCircle = CAShapeLayer()
progressCircle.path = circlePath.CGPath
progressCircle.lineWidth = progressWidth
progressCircle.strokeColor = progressCircleStrokeColor
progressCircle.fillColor = UIColor.clearColor().CGColor
self.layer.addSublayer(progressCircle)
let innerCirclePath = UIBezierPath(arcCenter: center, radius: radius-progressWidth, startAngle: CGFloat(0), endAngle:CGFloat(M_PI * 2), clockwise: true)
let innerCircle = CAShapeLayer()
innerCircle.path = innerCirclePath.CGPath
innerCircle.fillColor = innerCircleFillColor
self.layer.addSublayer(innerCircle)
}
List item
Here is the output got from my code:
Main problems faced in this code are
Phone is getting heat while drawing the circle
After drawning half of the circle drowning speed decreased
Please help me with an alternative
Try this :
import UIKit
class CircularProgressBar: UIView {
let shapeLayer = CAShapeLayer()
let secondShapeLayer = CAShapeLayer()
var circularPath: UIBezierPath?
override init(frame: CGRect) {
super.init(frame: frame)
print("Frame: \(self.frame)")
makeCircle()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
makeCircle()
}
func makeCircle(){
let circularPath = UIBezierPath(arcCenter: .zero, radius: self.bounds.width / 2, startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: true)
shapeLayer.path = circularPath.cgPath
shapeLayer.strokeColor = UIColor.orange.cgColor//UIColor.init(red: 0.0/255.0, green: 0.0/255.0, blue: 0.0/255.0, alpha: 1.0).cgColor
shapeLayer.lineWidth = 5.0
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineCap = kCALineCapRound
shapeLayer.strokeEnd = 0
shapeLayer.position = self.center
shapeLayer.transform = CATransform3DRotate(CATransform3DIdentity, -CGFloat.pi / 2, 0, 0, 1)
self.layer.addSublayer(shapeLayer)
}
func showProgress(percent: Float){
shapeLayer.strokeEnd = CGFloat(percent/100)
}
}
Take a UIView in Storyboard and add it as a subView. Then you can increase the progress using showProgress function.
You shouldn't add layers in drawRect:. Every time your view is drawn, you're adding a layer. That's why it's not surprising that your iPhone is suffering from it and is getting slower and hotter. You should create your layers in viewDidLoad or where your view is created, and you shouldn't modify them in drawRect. This method is only for drawing and nothing else.
I did it like this and worked for me. We need two layers, one for circle and another for progress.
private let circularProgressView = UIView()
private let circleLayer = CAShapeLayer()
private let progressLayer = CAShapeLayer()
private var circularPath: UIBezierPath?
private func setupCircularProgressBar() {
circularPath = UIBezierPath(arcCenter: CGPoint(x: circularProgressView.frame.size.width / 2.0,
y: circularProgressView.frame.size.height / 2.0),
radius: circularProgressView.bounds.width / 2, startAngle: -.pi / 2,
endAngle: 3 * .pi / 2, clockwise: true)
circleLayer.path = circularPath?.cgPath
circleLayer.fillColor = UIColor.clear.cgColor
circleLayer.lineCap = .round
circleLayer.lineWidth = 5
circleLayer.strokeColor = UIColor.darkGray.cgColor
progressLayer.path = circularPath?.cgPath
progressLayer.fillColor = UIColor.clear.cgColor
progressLayer.lineCap = .round
progressLayer.lineWidth = 3
progressLayer.strokeEnd = 0
progressLayer.strokeColor = UIColor.white.cgColor
circularProgressView.layer.addSublayer(circleLayer)
circularProgressView.layer.addSublayer(progressLayer)
}
private func animateCircularProgress(duration: TimeInterval) {
let circularProgressAnimation = CABasicAnimation(keyPath: "strokeEnd")
circularProgressAnimation.duration = duration
circularProgressAnimation.toValue = 1
circularProgressAnimation.fillMode = .forwards
circularProgressAnimation.isRemovedOnCompletion = false
progressLayer.add(circularProgressAnimation, forKey: "progressAnim")
}
usage is very simple you only call animateCircularProgress method with time interval parameter like this with 5 sec duration: animateCircularProgress(duration: 5)

Adding a CAShapeLayer around a button

I created a CAShapeLayer in the shape of a circle, and I want to add it around I button i have in the view. I am doing this instead of a border, due to animation purposes. I don't want a border around the button, I'd rather have a shape. This is how I am adding it, but for some reason, it is not adding the shape directly around the button.
This is my code to add the layer
recordLine = CAShapeLayer()
let circularPath = UIBezierPath(arcCenter: recordButton.center, radius: recordButton.frame.width / 2, startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: true)
recordLine.path = circularPath.cgPath
recordLine.strokeColor = UIColor.white.cgColor
recordLine.fillColor = UIColor.clear.cgColor
recordLine.lineWidth = 5
view.layer.addSublayer(recordLine)
This is how it is adding the line for some reason.
This is happening because you are adding Shape layer before rendering the autolayout Constrain properly.
Please add a single line before adding shape layer : self.view.layoutIfNeeded()
#IBOutlet weak var roundButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
roundButton.layer.cornerRadius = 50.0
roundButton.clipsToBounds = true
roundButton.alpha = 0.5
self.view.layoutIfNeeded()
let recordLine = CAShapeLayer()
let circularPath = UIBezierPath(arcCenter: roundButton.center, radius: roundButton.frame.width / 2, startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: false)
recordLine.path = circularPath.cgPath
recordLine.strokeColor = UIColor.black.cgColor
recordLine.fillColor = UIColor.clear.cgColor
recordLine.lineWidth = 10
view.layer.addSublayer(recordLine)
}
Please check the reference image
This is working for me.
My button is programmatic. I had to add the the cashapelayer in viewDidLayoutSubviews
lazy var cameraButton: UIButton = {
let button = UIButton(type: .system)
button.translatesAutoresizingMaskIntoConstraints = false
button.backgroundColor = UIColor.lightGray
return button
}()
var wasCAShapeLayerAddedToCameraButton = false
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if !wasCAShapeLayerAddedToCameraButton {
wasCAShapeLayerAddedToCameraButton = true
cameraButton.layer.cornerRadius = cameraButton.frame.width / 2
addCAShapeLayerToCameraButton()
}
}
func addCAShapeLayerToCameraButton() {
let circularPath = UIBezierPath(arcCenter: cameraButton.center,
radius: (cameraButton.frame.width / 2),
startAngle: 0,
endAngle: 2 * .pi,
clockwise: true)
shapeLayer.path = circularPath.cgPath
shapeLayer.strokeColor = UIColor.black.cgColor
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineWidth = 10
view.layer.addSublayer(shapeLayer)
}
// try like this i hope it will work for you.
Note: Color and other properties change as your requirement
let shapeLayer = CAShapeLayer()
shapeLayer.backgroundColor = UIColor.clear.cgColor
shapeLayer.name = "Star"
let path: CGPath = UIBezierPath(arcCenter: recordButton.center, radius: recordButton.frame.width / 2, startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: true)
shapeLayer.path = path
shapeLayer.lineWidth = 5.0
self.layer.mask = shapeLayer

Animate CAShapeLayer with circle UIBezierPath and CABasicAnimation

I'd like to animate a circle from angle 0 to 360 degrees in 15 sec.
The animation is weird. I know this is probably a start/end angle issue, I already faced that kind of problem with circle animations, but I don't know how to solve this one.
var circle_layer=CAShapeLayer()
var circle_anim=CABasicAnimation(keyPath: "path")
func init_circle_layer(){
let w=circle_view.bounds.width
let center=CGPoint(x: w/2, y: w/2)
//initial path
let start_angle:CGFloat = -0.25*360*CGFloat.pi/180
let initial_path=UIBezierPath(arcCenter: center, radius: w/2, startAngle: start_angle, endAngle: start_angle, clockwise: true)
initial_path.addLine(to: center)
//final path
let end_angle:CGFloat=start_angle+360*CGFloat(CGFloat.pi/180)
let final_path=UIBezierPath(arcCenter: center, radius: w/2, startAngle: start_angle, endAngle: end_angle, clockwise: true)
final_path.addLine(to: center)
//init layer
circle_layer.path=initial_path.cgPath
circle_layer.fillColor=UIColor(hex_code: "EA535D").cgColor
circle_view.layer.addSublayer(circle_layer)
//init anim
circle_anim.duration=15
circle_anim.fromValue=initial_path.cgPath
circle_anim.toValue=final_path.cgPath
circle_anim.isRemovedOnCompletion=false
circle_anim.fillMode=kCAFillModeForwards
circle_anim.delegate=self
}
func start_circle_animation(){
circle_layer.add(circle_anim, forKey: "circle_anim")
}
I want to start on top at 0 degrees and finish on top after a full tour:
You can't easily animate the fill of a UIBezierPath (or at least without introducing weird artifacts except in nicely controlled situations). But you can animate the strokeEnd of a path of the CAShapeLayer. And if you make the line width of the stroked path really wide (i.e. the radius of the final circle), and set the radius of the path to be half of that of the circle, you get something like what you're looking for.
private var circleLayer = CAShapeLayer()
private func configureCircleLayer() {
let radius = min(circleView.bounds.width, circleView.bounds.height) / 2
circleLayer.strokeColor = UIColor(hexCode: "EA535D").cgColor
circleLayer.fillColor = UIColor.clear.cgColor
circleLayer.lineWidth = radius
circleView.layer.addSublayer(circleLayer)
let center = CGPoint(x: circleView.bounds.width/2, y: circleView.bounds.height/2)
let startAngle: CGFloat = -0.25 * 2 * .pi
let endAngle: CGFloat = startAngle + 2 * .pi
circleLayer.path = UIBezierPath(arcCenter: center, radius: radius / 2, startAngle: startAngle, endAngle: endAngle, clockwise: true).cgPath
circleLayer.strokeEnd = 0
}
private func startCircleAnimation() {
circleLayer.strokeEnd = 1
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0
animation.toValue = 1
animation.duration = 15
circleLayer.add(animation, forKey: nil)
}
For ultimate control, when doing complex UIBezierPath animations, you can use CADisplayLink, avoiding artifacts that can sometimes result when using CABasicAnimation of the path:
private var circleLayer = CAShapeLayer()
private weak var displayLink: CADisplayLink?
private var startTime: CFTimeInterval!
private func configureCircleLayer() {
circleLayer.fillColor = UIColor(hexCode: "EA535D").cgColor
circleView.layer.addSublayer(circleLayer)
updatePath(percent: 0)
}
private func startCircleAnimation() {
startTime = CACurrentMediaTime()
displayLink = {
let _displayLink = CADisplayLink(target: self, selector: #selector(handleDisplayLink(_:)))
_displayLink.add(to: .current, forMode: .commonModes)
return _displayLink
}()
}
#objc func handleDisplayLink(_ displayLink: CADisplayLink) { // the #objc qualifier needed for Swift 4 #objc inference
let percent = CGFloat(CACurrentMediaTime() - startTime) / 15.0
updatePath(percent: min(percent, 1.0))
if percent > 1.0 {
displayLink.invalidate()
}
}
private func updatePath(percent: CGFloat) {
let w = circleView.bounds.width
let center = CGPoint(x: w/2, y: w/2)
let startAngle: CGFloat = -0.25 * 2 * .pi
let endAngle: CGFloat = startAngle + percent * 2 * .pi
let path = UIBezierPath()
path.move(to: center)
path.addArc(withCenter: center, radius: w/2, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.close()
circleLayer.path = path.cgPath
}
Then you can do:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
configureCircleLayer()
startCircleAnimation()
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
displayLink?.invalidate() // to avoid displaylink keeping a reference to dismissed view during animation
}
That yields:

Animate Custom UIView Property in Swift

all!
I have created a circular progress view using CoreGraphics that looks and updates like so:
50%
75%
The class is a UIView class, and it has a variable called 'progress' that determines how much of the circle is filled in.
It works well, but I want to be able to animate changes to the progress variable so that the bar animates smoothly.
I have read from myriad examples that I need to have a CALayer class along with the View class, which I have made, however, it doesn't animate at all.
Two questions:
Can I keep the graphic I drew in CoreGraphics, or do I need to somehow redraw it in CALayer?
My current (attempted) solution crashes towards the bottom at: anim.fromValue = pres.progress. What's up?
class CircleProgressView: UIView {
#IBInspectable var backFillColor: UIColor = UIColor.blueColor()
#IBInspectable var fillColor: UIColor = UIColor.greenColor()
#IBInspectable var strokeColor: UIColor = UIColor.greenColor()
dynamic var progress: CGFloat = 0.00 {
didSet {
self.layer.setValue(progress, forKey: "progress")
}
}
var distToDestination: CGFloat = 10.0
#IBInspectable var arcWidth: CGFloat = 20
#IBInspectable var outlineWidth: CGFloat = 5
override class func layerClass() -> AnyClass {
return CircleProgressLayer.self
}
override func drawRect(rect: CGRect) {
var fillColor = self.fillColor
if distToDestination < 3.0 {
fillColor = UIColor.greenColor()
} else {
fillColor = self.fillColor
}
//Drawing the inside of the container
//Drawing in the container
let center = CGPoint(x:bounds.width/2, y: bounds.height/2)
let radius: CGFloat = max(bounds.width, bounds.height) - 10
let startAngle: CGFloat = 3 * π / 2
let endAngle: CGFloat = 3 * π / 2 + 2 * π
let path = UIBezierPath(arcCenter: center, radius: radius/2 - arcWidth/2, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.lineWidth = arcWidth
backFillColor.setStroke()
path.stroke()
let fill = UIColor.blueColor().colorWithAlphaComponent(0.15)
fill.setFill()
path.fill()
//Drawing the fill path. Same process
let fillAngleLength = (π) * progress
let fillStartAngle = 3 * π / 2 - fillAngleLength
let fillEndAngle = 3 * π / 2 + fillAngleLength
let fillPath_fill = UIBezierPath(arcCenter: center, radius: radius/2 - arcWidth/2, startAngle: fillStartAngle, endAngle: fillEndAngle, clockwise: true)
fillPath_fill.lineWidth = arcWidth
fillColor.setStroke()
fillPath_fill.stroke()
//Drawing container outline on top
let outlinePath_outer = UIBezierPath(arcCenter: center, radius: radius / 2 - outlineWidth / 2, startAngle: startAngle, endAngle: endAngle, clockwise: true)
let outlinePath_inner = UIBezierPath(arcCenter: center, radius: radius / 2 - arcWidth + outlineWidth / 2, startAngle: startAngle, endAngle: endAngle, clockwise: true)
outlinePath_outer.lineWidth = outlineWidth
outlinePath_inner.lineWidth = outlineWidth
strokeColor.setStroke()
outlinePath_outer.stroke()
outlinePath_inner.stroke()
}
}
class CircleProgressLayer: CALayer {
#NSManaged var progress: CGFloat
override class func needsDisplayForKey(key: String) -> Bool {
if key == "progress" {
return true
}
return super.needsDisplayForKey(key)
}
override func actionForKey(key: String) -> CAAction? {
if (key == "progress") {
if let pres = self.presentationLayer() {
let anim: CABasicAnimation = CABasicAnimation.init(keyPath: key)
anim.fromValue = pres.progress
anim.duration = 0.2
return anim
}
return super.actionForKey(key)
} else {
return super.actionForKey(key)
}
}
}
Thanks for the help!
Try this out :)
class ViewController: UIViewController {
let progressView = CircleProgressView(frame:CGRect(x: 0.0, y: 0.0, width: 200.0, height: 200))
override func viewDidLoad() {
super.viewDidLoad()
let button = UIButton()
button.frame = CGRectMake(0, 300, 200, 100)
button.backgroundColor = UIColor.yellowColor()
button.addTarget(self, action: #selector(ViewController.tap), forControlEvents: UIControlEvents.TouchUpInside)
view.addSubview(button)
view.addSubview(progressView)
progressView.progress = 1.0
}
func tap() {
if progressView.progress == 0.5 {
progressView.progress = 1.0
} else {
progressView.progress = 0.5
}
}
}
class CircleProgressView: UIView {
dynamic var progress: CGFloat = 0.00 {
didSet {
let animation = CABasicAnimation()
animation.keyPath = "progress"
animation.fromValue = circleLayer().progress
animation.toValue = progress
animation.duration = Double(0.5)
self.layer.addAnimation(animation, forKey: "progress")
circleLayer().progress = progress
}
}
func circleLayer() -> CircleProgressLayer {
return self.layer as! CircleProgressLayer
}
override class func layerClass() -> AnyClass {
return CircleProgressLayer.self
}
}
class CircleProgressLayer: CALayer {
#NSManaged var progress: CGFloat
override class func needsDisplayForKey(key: String) -> Bool {
if key == "progress" {
return true
}
return super.needsDisplayForKey(key)
}
var backFillColor: UIColor = UIColor.blueColor()
var fillColor: UIColor = UIColor.greenColor()
var strokeColor: UIColor = UIColor.greenColor()
var distToDestination: CGFloat = 10.0
var arcWidth: CGFloat = 20
var outlineWidth: CGFloat = 5
override func drawInContext(ctx: CGContext) {
super.drawInContext(ctx)
UIGraphicsPushContext(ctx)
//Drawing in the container
let center = CGPoint(x:bounds.width/2, y: bounds.height/2)
let radius: CGFloat = max(bounds.width, bounds.height) - 10
let startAngle: CGFloat = 3 * CGFloat(M_PI) / 2
let endAngle: CGFloat = 3 * CGFloat(M_PI) / 2 + 2 * CGFloat(M_PI)
let path = UIBezierPath(arcCenter: center, radius: radius/2 - arcWidth/2, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.lineWidth = arcWidth
backFillColor.setStroke()
path.stroke()
let fill = UIColor.blueColor().colorWithAlphaComponent(0.15)
fill.setFill()
path.fill()
//Drawing the fill path. Same process
let fillAngleLength = (CGFloat(M_PI)) * progress
let fillStartAngle = 3 * CGFloat(M_PI) / 2 - fillAngleLength
let fillEndAngle = 3 * CGFloat(M_PI) / 2 + fillAngleLength
let fillPath_fill = UIBezierPath(arcCenter: center, radius: radius/2 - arcWidth/2, startAngle: fillStartAngle, endAngle: fillEndAngle, clockwise: true)
fillPath_fill.lineWidth = arcWidth
fillColor.setStroke()
fillPath_fill.stroke()
//Drawing container outline on top
let outlinePath_outer = UIBezierPath(arcCenter: center, radius: radius / 2 - outlineWidth / 2, startAngle: startAngle, endAngle: endAngle, clockwise: true)
let outlinePath_inner = UIBezierPath(arcCenter: center, radius: radius / 2 - arcWidth + outlineWidth / 2, startAngle: startAngle, endAngle: endAngle, clockwise: true)
outlinePath_outer.lineWidth = outlineWidth
outlinePath_inner.lineWidth = outlineWidth
strokeColor.setStroke()
outlinePath_outer.stroke()
outlinePath_inner.stroke()
UIGraphicsPopContext()
}
}
Whilst AntonTheDev provides a great answer, his solution does not allow you to animate the CircularProgressView in an animation block, so you cant do neat things like:
UIView.animate(withDuration: 2, delay: 0, options: .curveEaseInOut,
animations: {
circularProgress.progress = 0.76
}, completion: nil)
There's a similar question here with a up to date Swift 3 answer based on ideas from the accepted answer and this post.
This is what the final solution looks like.
Swift 3 Solution

Resources