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
Related
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.
What i want to achieve is
What i tried is to draw three arcs with different colors with lineCapStyle .rounded. My code for drawing these arcs are below
private func circularActivityPath(rect:CGRect, configuration:PathConfiguration)-> CGPath {
let center = CGPoint(x: rect.maxX / 2, y: rect.maxY / 2)
let longestSide = rect.height < rect.width ? rect.height : rect.width
let path = UIBezierPath(arcCenter: center, radius: (longestSide / 2) - (configuration.lineWidth / 2), startAngle: configuration.startAngle.deg2rad() , endAngle: configuration.endAngle.deg2rad(), clockwise: true)
path.lineCapStyle = .round
return path.cgPath
}
override func draw(_ rect: CGRect) {
let config1 = PathConfiguration(color: .red, lineWidth: lineWidth, startAngle: CGFloat(-90), endAngle: CGFloat(10), type: .track , shape: shape )
let trackLayer1 = CAShapeLayer()
trackLayer1.drawActivityCircles(in: rect, configuration: config1)
let config2 = PathConfiguration(color: .blue, lineWidth: lineWidth, startAngle: CGFloat(25), endAngle: CGFloat(80), type: .track , shape: shape )
let trackLayer2 = CAShapeLayer()
trackLayer2.drawActivityCircles(in: rect, configuration: config2)
let config3 = PathConfiguration(color: .green, lineWidth: lineWidth, startAngle: CGFloat(95), endAngle: CGFloat(255), type: .track , shape: shape )
let trackLayer3 = CAShapeLayer()
trackLayer3.drawActivityCircles(in: rect, configuration: config3)
self.layer.addSublayer(trackLayer2) // blue
self.layer.addSublayer(trackLayer3) // green
self.layer.addSublayer(trackLayer1) // red
}
where PathConfiguration is a struct
struct PathConfiguration {
let color: UIColor
let lineWidth: CGFloat
let startAngle: CGFloat
let endAngle: CGFloat
let type: TrackType
let shape: TrackShape
}
What i get is below with rounded caps on both sides... i want to achieve one rounded and one arc cap on respective ends. i will be very thankful to you if i get some pointers how i can get the same shape
See the below code to achieve this result.
import UIKit
// MARK: - Enums
public enum AnimationStyle: Int {
case animationFanAll
case animationFan
case animationFadeIn
case animationthreeD
case none
}
public enum PercentageStyle : Int {
case none
case inward
case outward
case over
}
open class Circular: UIView {
// MARK: - Public Properties
public var animationType: AnimationStyle {
get {
return _animationType
}
set(newValue) {
_animationType = newValue
setNeedsDisplay()
}
}
public var showPercentageStyle: PercentageStyle {
get {
return _showPercentageStyle
}
set(newValue) {
_showPercentageStyle = newValue
setNeedsDisplay()
}
}
public var lineWidth: CGFloat {
get {
return _lineWidth
}
set(newValue) {
_lineWidth = newValue
setNeedsDisplay()
}
}
// MARK:- Private Variable
private var _percentages: [Double]
private var _colors: [UIColor]
private var _lineWidth = CGFloat( 10.0)
private var _animationType: AnimationStyle
private var _showPercentageStyle: PercentageStyle
//MARK:- draw
override public func draw(_ rect: CGRect) {
var startAngle = -90.0
for i in 0..<_percentages.count {
let endAngle = startAngle + ( _percentages[i] * 3.6 ) - 4
let shapeLayer = self.addArac(with: _colors[i], in: rect, startAngle: startAngle, endAngle: endAngle)
showAnimationStyle(index: Double(i), shapeLayer: shapeLayer, startAngle: startAngle, endAngle: endAngle)
showPercentages(midAngel:startAngle + (endAngle - startAngle)/2, percentage: _percentages[i])
startAngle = (endAngle + 4 )
}
}
//MARK:- inializer
public init(percentages:[Double],colors:[UIColor],aimationType:AnimationStyle = .animationFanAll , showPercentageStyle: PercentageStyle = .none) {
self._percentages = percentages
self._colors = colors
self._animationType = aimationType
self._showPercentageStyle = showPercentageStyle
super.init(frame:CGRect.zero)
self.backgroundColor = .clear
self.clipsToBounds = false
}
required public init?(coder: NSCoder) {
// super.init(coder: coder)
fatalError("init(coder:) has not been implemented")
}
//MARK:- Animations Functions
private func showAnimationStyle(index:Double,shapeLayer:CAShapeLayer,startAngle:Double,endAngle:Double) {
switch _animationType {
case .animationFanAll:
maskEachLayerAnimation(startAngal: startAngle, endAngal: endAngle + 4 , shape: shapeLayer)
case .animationFan:
if Int(index) == _percentages.count - 1 {
maskAnimation()
}
case .animationFadeIn:
oppacityAnimation(index: index, shape: shapeLayer)
case .animationthreeD:
transformAnimation(index: index, shape: shapeLayer)
case .none:
break
}
}
private func oppacityAnimation(index:Double,shape:CAShapeLayer) {
shape.opacity = 0
DispatchQueue.main.asyncAfter(deadline: .now() + Double(index)/2.5 ) {
shape.opacity = 1
let animation = CABasicAnimation(keyPath: "opacity")
animation.fromValue = 0
animation.toValue = 1
animation.duration = 1
shape.add(animation, forKey: nil)
}
}
private func transformAnimation(index:Double,shape:CAShapeLayer){
shape.opacity = 0
DispatchQueue.main.asyncAfter(deadline: .now() + Double(index)/2.5 ) {
shape.opacity = 1
let animation = CABasicAnimation(keyPath: "transform")
animation.fromValue = CATransform3DMakeScale(0, 0, 1)
animation.toValue = CATransform3DIdentity
animation.duration = 1
shape.add(animation, forKey: nil)
}
}
private func maskEachLayerAnimation(startAngal:Double,endAngal:Double,shape:CAShapeLayer){
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor.green.cgColor
shapeLayer.lineWidth = max( bounds.maxX,bounds.maxY)/5
shapeLayer.frame = bounds
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let longestSide = max(bounds.height,bounds.width)
shapeLayer.path = UIBezierPath(arcCenter: center, radius: longestSide/2, startAngle: CGFloat(startAngal).deg2rad(), endAngle: CGFloat(endAngal ).deg2rad(), clockwise: true).cgPath
shapeLayer.strokeEnd = 0
shape.mask = shapeLayer
addAnimationToLayer(toLayer: shape, fromLayer: shapeLayer)
}
private func maskAnimation() {
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor.white.cgColor
shapeLayer.lineWidth = max( bounds.maxX,bounds.maxY)/2
shapeLayer.frame = bounds
let path = UIBezierPath(arcCenter: CGPoint(x:bounds.midX,y:bounds.midY), radius:max( bounds.maxX/2,bounds.maxY/2), startAngle: CGFloat(-89.0).deg2rad(), endAngle: CGFloat( 270.0).deg2rad(), clockwise: true)
shapeLayer.path = path.cgPath
shapeLayer.strokeEnd = 0
self.layer.mask = shapeLayer
addAnimationToLayer(toLayer: self.layer, fromLayer: shapeLayer)
}
private func addAnimationToLayer(toLayer:CALayer , fromLayer:CAShapeLayer) {
CATransaction.begin()
CATransaction.setCompletionBlock {
toLayer.mask = nil
}
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.beginTime = CACurrentMediaTime() + 0.3
animation.fromValue = 0
animation.toValue = 1
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
animation.duration = 1
fromLayer.add(animation, forKey: "line")
CATransaction.commit()
}
//MARK:- show percentages
private func showPercentages(midAngel:Double, percentage:Double) {
guard let radius = getRadiusOfPercentage() else {
return
}
let center = CGPoint(x: bounds.maxX / 2, y: bounds.maxY / 2)
let x = center.x + (radius) * CGFloat(cos(CGFloat(midAngel).deg2rad()))
let y = center.y + (radius) * CGFloat(sin(CGFloat(midAngel).deg2rad()))
let percentageLabel = UILabel(frame: CGRect.zero)
percentageLabel.frame = CGRect.zero
percentageLabel.text = String(percentage)
percentageLabel.textColor = .black
percentageLabel.font = UIFont.boldSystemFont(ofSize: 12)
percentageLabel.sizeToFit()
percentageLabel.center = CGPoint(x:x,y:y)
addSubview(percentageLabel)
percentageLabel.alpha = 0
var delay = 1.5
if self.animationType == .none {
delay = 0
}
UIView.animate(withDuration: 0.5, delay: delay, options: .curveEaseOut, animations: {
percentageLabel.alpha = 1
})
}
private func getRadiusOfPercentage() -> CGFloat? {
let longestSide = max(bounds.height,bounds.width)
switch self.showPercentageStyle {
case .inward:
return longestSide/3 - lineWidth
case .over:
return longestSide/2 - lineWidth
case .outward:
return longestSide/2 + lineWidth + 5
case .none:
return nil
}
}
//MARK:- Drawing Code
private func addArac(with color:UIColor ,in rect:CGRect, startAngle:Double , endAngle:Double)-> CAShapeLayer {
let center = CGPoint(x: rect.maxX / 2, y: rect.maxY / 2)
let longestSide = max(rect.height,rect.width)
let lineWidth = CGFloat(self._lineWidth / 20)
let smallCircleRadious = (longestSide / (2 + lineWidth))
let startAngle = CGFloat(startAngle)
let endAngle = CGFloat(endAngle)
let outerRadious = (longestSide / 2)
let midPoint = (longestSide / (2 + lineWidth/2.7))
let path = UIBezierPath()
let x3 = center.x + (outerRadious) * CGFloat(cos(startAngle.deg2rad()))
let y3 = center.y + (outerRadious) * CGFloat(sin(startAngle.deg2rad()))
let x4 = center.x + (smallCircleRadious) * CGFloat(cos(startAngle.deg2rad()))
let y4 = center.y + (smallCircleRadious) * CGFloat(sin(startAngle.deg2rad()))
let x5 = center.x + (midPoint) * CGFloat(cos((startAngle + self._lineWidth * 0.5).deg2rad()))
let y5 = center.y + (midPoint) * CGFloat(sin((startAngle + self._lineWidth * 0.5).deg2rad()))
path.move(to: CGPoint(x:x4,y:y4))
path.addQuadCurve(to: CGPoint(x:x3,y:y3), controlPoint: CGPoint(x:x5,y:y5))
path.addArc(withCenter:center, radius:outerRadious, startAngle: startAngle.deg2rad() , endAngle: endAngle.deg2rad(), clockwise: true)
let x1 = center.x + (outerRadious) * CGFloat(cos(endAngle.deg2rad()))
let y1 = center.y + (outerRadious) * CGFloat(sin(endAngle.deg2rad()))
let x6 = center.x + (midPoint) * CGFloat(cos((endAngle + self._lineWidth * 0.6).deg2rad()))
let y6 = center.y + (midPoint) * CGFloat(sin((endAngle + self._lineWidth * 0.6).deg2rad()))
let x2 = center.x + (smallCircleRadious) * CGFloat(cos(endAngle.deg2rad()))
let y2 = center.y + (smallCircleRadious) * CGFloat(sin(endAngle.deg2rad()))
path.move(to: CGPoint(x:x1,y:y1))
path.addQuadCurve(to: CGPoint(x:x2,y:y2), controlPoint: CGPoint(x:x6,y:y6))
path.addArc(withCenter:center, radius: smallCircleRadious, startAngle: endAngle.deg2rad(), endAngle: startAngle.deg2rad(), clockwise: false)
let shape = CAShapeLayer()
shape.frame = bounds
shape.lineCap = .round
shape.fillColor = color.cgColor
shape.path = path.cgPath
layer.addSublayer(shape)
return shape
}
}
extension CGFloat {
func deg2rad() -> CGFloat {
return self * .pi / 180
}
}
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
}
}
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:
I am trying to Implement drawing of Arc from certain start angle to certain end angle. I am trying to animate drawing of arc but have no luck.
I have looked for couple of implementations but all of them refers to be drawing of animating boder of circle.
Like drawing a pie but with one color. With certain start and end angle. Animated drawing of pie.
What I like is to have have animated filling while arc is being animated from start angle to end angle.
Like arc is being drawn clockwise and while drawing it is filling its inside color.
Any idea How can I acheive this in Swift?
Thanks in advance.
Edit
This is what I have drawn:
(gray color shows area covered by drawing pie)
Code for drawing Pie
import Foundation
import UIKit
#IBDesignable class PieChart : UIView {
#IBInspectable var percentage: CGFloat = 50 {
didSet {
self.setNeedsDisplay()
}
}
#IBInspectable var outerBorderWidth: CGFloat = 2 {
didSet {
self.setNeedsDisplay()
}
}
#IBInspectable var outerBorderColor: UIColor = UIColor.white {
didSet {
self.setNeedsDisplay()
}
}
#IBInspectable var percentageFilledColor: UIColor = UIColor.primary {
didSet {
self.setNeedsDisplay()
}
}
let circleLayer = CAShapeLayer()
override func draw(_ rect: CGRect) {
drawPie(rect: rect, endPercent: percentage, color: UIColor.primary)
}
func drawPie(rect: CGRect, endPercent: CGFloat = 70, color: UIColor) {
let center = CGPoint(x: rect.origin.x + rect.width / 2, y: rect.origin.y + rect.height / 2)
let radius = min(rect.width, rect.height) / 2
let gpath = UIBezierPath(arcCenter: center, radius: radius, startAngle: 0, endAngle: CGFloat(360.degreesToRadians), clockwise: true)
UIColor.white.setFill()
gpath.fill()
let circlePath = UIBezierPath(arcCenter: center, radius: radius, startAngle: CGFloat(0), endAngle:CGFloat(360.degreesToRadians), clockwise: true)
circleLayer.path = circlePath.cgPath
circleLayer.fillColor = UIColor.clear.cgColor
circleLayer.strokeColor = outerBorderColor.cgColor
circleLayer.lineWidth = outerBorderWidth
circleLayer.borderColor = outerBorderColor.cgColor
self.layer.addSublayer(circleLayer)
let π: CGFloat = 3.14
let halfPi: CGFloat = π / 2
let startPercent: CGFloat = 0.0
let startAngle = (startPercent / 100 * π * 2 - π ) + halfPi
let endAngle = (endPercent / 100 * π * 2 - π ) + halfPi
let path = UIBezierPath()
path.move(to: center)
path.addArc(withCenter: center, radius: radius, startAngle: CGFloat(startAngle), endAngle: CGFloat(endAngle), clockwise: true)
path.close()
percentageFilledColor.setFill()
path.fill()
}
}
I've created this code using playground and swift. This code will draw a pie chart animating the filling color. Make sure you have the Timeline pane open on Xcode to see the animation working. I couldn't find a way to pass the path function directly to CAAnimation so my workaround was to create a list of steps, each one containing the path for a percentage stage. You can change the steps for a smother animation.
//: Playground - noun: a place where people can play
import UIKit
import XCPlayground
let STEPS_ANIMATION = 50
let initialPercentage : CGFloat = 0.10
let finalPercentage : CGFloat = 0.75
func buildPiePath(frame : CGRect, percentage : CGFloat) -> UIBezierPath {
let newPath = UIBezierPath()
let startPoint = CGPoint(x: frame.size.width, y: frame.height / 2.0)
let centerPoint = CGPoint(x: frame.size.width / 2.0, y: frame.height / 2.0)
let startAngle : CGFloat = 0
let endAngle : CGFloat = percentage * 2 * CGFloat(M_PI)
newPath.move(to: centerPoint)
newPath.addLine(to: startPoint)
newPath.addArc(withCenter: centerPoint,
radius: frame.size.width / 2.0,
startAngle: startAngle,
endAngle: endAngle,
clockwise: true)
newPath.addLine(to: centerPoint)
newPath.close()
return newPath
}
func buildPiePathList(frame: CGRect,
startPercentage: CGFloat,
finalPercentage: CGFloat) -> [AnyObject] {
var listValues = [AnyObject]()
for index in 1...STEPS_ANIMATION {
let delta = finalPercentage - startPercentage
let currentPercentage = CGFloat(index) / CGFloat(STEPS_ANIMATION)
let percentage = CGFloat(startPercentage + (delta * currentPercentage))
listValues.append(buildPiePath(frame: frame,
percentage: percentage)
.cgPath)
}
return listValues
}
// Container for pie chart
let container = UIView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))
container.backgroundColor = UIColor.gray
XCPShowView(identifier: "Container View", view: container)
let circleFrame = CGRect(x: 20, y: 20, width: 360, height: 360)
// Red background
let background = CAShapeLayer()
background.frame = circleFrame
background.fillColor = UIColor.red.cgColor
background.path = buildPiePath(frame: circleFrame, percentage: 1.0).cgPath
container.layer.addSublayer(background)
// Green foreground that animates
let foreground = CAShapeLayer()
foreground.frame = circleFrame
foreground.fillColor = UIColor.green.cgColor
foreground.path = buildPiePath(frame: circleFrame, percentage: initialPercentage).cgPath
container.layer.addSublayer(foreground)
// Filling animation
let fillAnimation = CAKeyframeAnimation(keyPath: "path")
fillAnimation.values = buildPiePathList(frame: circleFrame,
startPercentage: initialPercentage,
finalPercentage: finalPercentage)
fillAnimation.duration = 3
fillAnimation.calculationMode = kCAAnimationDiscrete
foreground.add(fillAnimation, forKey:nil)