Progress bar with variable time goals over time - ios

I am totally new to Swift and a noob at coding, so this might be really simple. I'm creating a progress circle which I want to count up to 100% as 24 hours passes, then the progress resets and counts up until tree days has passed from the original starting point, then one week... So far I got the circle and can controll the progress manually, but I'm not sure where to start to get the time to controll it. Hope someone is able to help.
lass ViewController: UIViewController {
let shapeLayer = CAShapeLayer()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let center = view.center
// Create my track
let trackLayer = CAShapeLayer()
let circularPath = UIBezierPath(arcCenter: center, radius: 100, startAngle: -CGFloat.pi / 2, endAngle: 2 * CGFloat.pi, clockwise: true)
trackLayer.path = circularPath.cgPath
trackLayer.strokeColor = UIColor.lightGray.cgColor
trackLayer.lineWidth = 20
trackLayer.fillColor = UIColor.clear.cgColor
trackLayer.lineCap = kCALineCapRound
view.layer.addSublayer(trackLayer)
// Create progress circle
shapeLayer.path = circularPath.cgPath
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.lineWidth = 20
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineCap = kCALineCapRound
shapeLayer.strokeEnd = 0
view.layer.addSublayer(shapeLayer)
print ("Atempting to animate stroke")
let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
basicAnimation.toValue = 1
basicAnimation.duration = 1.2
basicAnimation.fillMode = kCAFillModeForwards
basicAnimation.isRemovedOnCompletion = false
shapeLayer.add(basicAnimation, forKey: "urSoBasic")
}

You only need to reconfigure the duration so suppose you want it to fill in 60 seconds then refill it again in 10 seconds so you can do ( you can create a function but this for illustration also you can use a timer to easily invalidate it )
let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
basicAnimation.toValue = 1
basicAnimation.duration = 60
basicAnimation.fillMode = kCAFillModeForwards
basicAnimation.isRemovedOnCompletion = false
shapeLayer.add(basicAnimation, forKey: "urSoBasic")
DispatchQueue.main.asyncAfter(deadline: .now() + 60 , execute: {
self.shapeLayer.removeAllAnimations()
let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
basicAnimation.toValue = 1
basicAnimation.duration = 10
basicAnimation.fillMode = kCAFillModeForwards
basicAnimation.isRemovedOnCompletion = false
self.shapeLayer.add(basicAnimation, forKey: "urSoBasic")
})

Related

Center of CAGradientLayer returns wrong position on rotate animation

I have a circle like in Instagram profile image which has story. I want to have an affect like circle is spinning. For that, I used CABasicAnimation. It is spinning but not in center.
As I searched, I need to give bounty for shapeLayer but when I do that, It doesn't placed where It needs to be.
How can I achieve animation like in Instagram story circle (like circle is spinning)?
EDIT I also try to add "colors" animation but because It works like It is in square, I can't get the desired result.
func addCircle() {
let circularPath = UIBezierPath(arcCenter: CGPoint(x: self.bounds.midX, y: self.bounds.midY), radius: self.bounds.width / 2, startAngle: -CGFloat.pi / 2, endAngle: 2 * CGFloat.pi, clockwise: true)
shapeLayer.strokeColor = UIColor.blue.cgColor
shapeLayer.lineWidth = 10
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineCap = kCALineCapRound
shapeLayer.strokeEnd = 1.0
gradient.frame = circularPath.bounds
gradient.colors = [UIColor.blue.cgColor, UIColor.red.cgColor]
gradient.startPoint = CGPoint(x: 0, y: 1)
gradient.endPoint = CGPoint(x: 1, y: 0)
shapeLayer.path = circularPath.cgPath
gradient.mask = shapeLayer
self.layer.addSublayer(gradient)
}
let rotationAnimation: CAAnimation = {
let animation = CABasicAnimation(keyPath: "transform.rotation")
animation.fromValue = 0
animation.toValue = 1
animation.duration = 4
animation.repeatCount = MAXFLOAT
return animation
}()
#objc private func handleTap() {
print("Attempting to animate stroke")
shapeLayer.add(rotationAnimation, forKey: "urSoBasic2")
}
If spinning, why not spin gradient
let rotationAnimation: CAAnimation = {
let animation = CABasicAnimation(keyPath: "transform.rotation")
animation.fromValue = 0
animation.toValue = Float.pi * 2
animation.duration = 4
animation.repeatCount = MAXFLOAT
return animation
}()
#objc func handleTap() {
print("Attempting to animate stroke")
gradient.add(rotationAnimation, forKey: "urSoBasic2")
}
As to off center problem. The correct frame for gradient should be like this :
gradient.frame = self.bounds
To verify the center, you may add a background for the view:
self.background = UIColor.black.
The reason is width and height of the view is not set well due to the constraints. Try to set the correct bounds of the view, so when you add the circularPath, it's center in the view.

How to reverse draw animation for circle?

I have created draw circle animation.
When user tap touchdown on button the animation goes to 100% , but when user move finger away it pauses.
What i want to do now is to make this animation goes to 0% (start value) instead of pause.
var shapeLayer = CAShapeLayer()
var cabAnim = CABasicAnimation()
var firstAnimation = true
override func viewDidLoad() {
super.viewDidLoad()
let button = UIButton(frame: CGRect(x: 100, y: 200, width: 100, height: 50))
button.backgroundColor = .blue
button.setTitle("123", for: .normal)
button.addTarget(self, action: #selector(startCircleAnimation), for: .touchDown)
button.addTarget(self, action: #selector(endCircleAnimation), for: .touchUpInside)
self.view.addSubview(button)
let circlePath = UIBezierPath(arcCenter: CGPoint(x: 0, y: 0), radius: CGFloat(50), startAngle: -CGFloat(Double.pi/2), endAngle: CGFloat(Double.pi * 3/2), clockwise: true)
shapeLayer.path = circlePath.cgPath
shapeLayer.position = CGPoint(x: self.view.frame.midX, y: self.view.frame.midY)
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.lineWidth = 3.0
shapeLayer.strokeEnd = 0.0
self.view.layer.addSublayer(shapeLayer)
}
#objc func startCircleAnimation() {
if (firstAnimation) {
firstAnimation = false
circleAnimation()
} else {
resumeAnimation(layer: shapeLayer)
}
}
#objc func endCircleAnimation() {
pauseAnimation(layer: shapeLayer)
}
func circleAnimation() {
cabAnim = CABasicAnimation(keyPath: "strokeEnd")
cabAnim.duration = 1.5
cabAnim.repeatCount = 1
cabAnim.fromValue = 0.0
cabAnim.toValue = 1.0
let cam = CAMediaTimingFunctionName.linear
cabAnim.timingFunction = CAMediaTimingFunction(name: cam)
shapeLayer.strokeEnd = 1.0
shapeLayer.add(cabAnim, forKey: "animateCircle")
}
func pauseAnimation(layer: CALayer) {
let pausedTime: CFTimeInterval = layer.convertTime(CACurrentMediaTime(), from: nil)
layer.speed = 0.0
layer.timeOffset = pausedTime
}
func resumeAnimation(layer: CALayer) {
let resAnimation = CABasicAnimation(keyPath: "strokeEnd")
let pausedTime: CFTimeInterval = layer.timeOffset
layer.speed = 1.0
layer.timeOffset = 0.0
layer.beginTime = 0.0
let timeSincePause: CFTimeInterval = layer.convertTime(CACurrentMediaTime(), from: nil) - pausedTime
layer.beginTime = timeSincePause
}
How can i make value goes to 0 instead of pause?
Edit:
I want it to move from currentValue to 0 (for example i hold button animation goes to 50% and then i remove finger from button and animation goes to 0%)
you can use this function to create the backward animation, it will draw animation from 100 to 0. i have added the whole project you can take a look.
#IBAction func backward(_ sender: Any) {
let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
basicAnimation.fromValue = 1
basicAnimation.toValue = 0
basicAnimation.duration = 2
basicAnimation.fillMode = kCAFillModeBackwards
basicAnimation.isRemovedOnCompletion = false
// Callback function
CATransaction.setCompletionBlock {
print("end animation")
}
shapeLayer.add(basicAnimation, forKey: "urSoBasic")
}
Create ShapeLayer
in most cases, you can call creating shape layer on `ViewDidLoad`
func createShape() {
// let's start by drawing a circle somehow
let center = view.center
let trackLayer = CAShapeLayer()
let circularPath = UIBezierPath(arcCenter: center, radius: 100, startAngle: -CGFloat.pi / 2, endAngle: 2 * CGFloat.pi, clockwise: true)
trackLayer.path = circularPath.cgPath
trackLayer.strokeColor = UIColor.lightGray.cgColor
trackLayer.lineWidth = 10
trackLayer.fillColor = UIColor.clear.cgColor
trackLayer.lineCap = kCALineCapRound
view.layer.addSublayer(trackLayer)
shapeLayer.path = circularPath.cgPath
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.lineWidth = 10
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineCap = kCALineCapRound
shapeLayer.strokeEnd = 0
view.layer.addSublayer(shapeLayer)
}
Forward Action
#IBAction func forward(_ sender: Any) {
let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
basicAnimation.toValue = 1
basicAnimation.duration = 2
basicAnimation.fillMode = kCAFillModeForwards
basicAnimation.isRemovedOnCompletion = false
shapeLayer.add(basicAnimation, forKey: "urSoBasic")
}
Backward Action
#IBAction func backward(_ sender: Any) {
let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
basicAnimation.fromValue = 1
basicAnimation.toValue = 0
basicAnimation.duration = 2
basicAnimation.fillMode = kCAFillModeBackwards
basicAnimation.isRemovedOnCompletion = false
// Callback function
CATransaction.setCompletionBlock {
print("end animation")
}
shapeLayer.add(basicAnimation, forKey: "urSoBasic")
}
On hold or resume logics, you can play with these codes.

How draw a circular progress bar with CAShapLayer and auto layout constraints

I'm drawing a circular progress bar using CAShapeLayer, I can produce it only if I center it on my view. Now I want to constraint it to the trailing edge of my card view. Below is my code:
let center = cardView.center
//track layer
let trackLayer = CAShapeLayer()
let circularPath = UIBezierPath(arcCenter: center, radius: 100, startAngle: -CGFloat.pi / 2, endAngle: 2 * CGFloat.pi, clockwise: true)
trackLayer.path = circularPath.cgPath
trackLayer.strokeColor = UIColor.gray.cgColor
trackLayer.strokeEnd = 1
trackLayer.lineCap = kCALineCapRound
trackLayer.fillColor = UIColor.clear.cgColor
trackLayer.lineWidth = 5
cardView.layer.addSublayer(trackLayer)
//progress Layer
let shapeLayer = CAShapeLayer()
shapeLayer.path = circularPath.cgPath
shapeLayer.strokeColor = UIColor.yellow.cgColor
//NumberFormatter().number(from: percentageUsed) as! CGFloat
shapeLayer.strokeEnd = 0.6
shapeLayer.lineCap = kCALineCapRound
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineWidth = 5
cardView.layer.addSublayer(shapeLayer)
Now I wish i could do something like
shapeLayer.leadingAnchor.constraint(equalTo: cardVeiw.leadingAnchor, constant: 20).isActive = true

how to move position of shape layer (swift4)

My code right now uses a circular progress view and it places in dead center of the screen. I need this exact circular progress view to move about 150 pixels down the x axis.
import UIKit
class ViewController: UIViewController {
let shapeLayer = CAShapeLayer()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let center = view.center
let trackLayer = CAShapeLayer()
let circularPath = UIBezierPath(arcCenter: center, radius: 100, startAngle: -CGFloat.pi / 2, endAngle: 2 * CGFloat.pi, clockwise: true)
trackLayer.path = circularPath.cgPath
trackLayer.strokeColor = UIColor.lightGray.cgColor
trackLayer.lineWidth = 10
trackLayer.fillColor = UIColor.clear.cgColor
trackLayer.lineCap = kCALineCapRound
view.layer.addSublayer(trackLayer)
shapeLayer.path = circularPath.cgPath
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.lineWidth = 10
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineCap = kCALineCapRound
shapeLayer.strokeEnd = 0
view.layer.addSublayer(shapeLayer)
}
You can try , you say down Y
let center = CGPoint(x:view.center.x,y:view.center.y + 150)
Or right x
let center = CGPoint(x:view.center.x + 150 ,y:view.center.y)

Circular progress in swift

I am trying to make a simple circular progress bar in swift. What I have failed to do so far is, the progress should start at the top of the circle (at the moment it is starting at the 90 degree point). I would also like to have a circular line under the progress line that should be thicker than the progress line and have a different color as well..
Can anyone put me in the right direction towards my wishes?
Here is my function:
func animateView() {
let circle = viewProgress // viewProgress is a UIView
var progressCircle = CAShapeLayer()
let circlePath = UIBezierPath(ovalInRect: circle.bounds)
progressCircle = CAShapeLayer ()
progressCircle.path = circlePath.CGPath
progressCircle.strokeColor = UIColor.greenColor().CGColor
progressCircle.fillColor = UIColor.clearColor().CGColor
progressCircle.lineWidth = 5.0
circle.layer.addSublayer(progressCircle)
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0
animation.toValue = 1.5
animation.duration = 1
animation.fillMode = kCAFillModeForwards
animation.removedOnCompletion = false
progressCircle.addAnimation(animation, forKey: "ani")
}
Igor this is how it looks:
For Swift 4
Declare two CAShapeLayers. One for actual circle and another for progress Layer!
func createCircularPath() {
let circleLayer = CAShapeLayer()
let progressLayer = CAShapeLayer()
let center = self.view.center
let circularPath = UIBezierPath(arcCenter: center, radius: 100, startAngle: -.pi / 2, endAngle: 2 * .pi, clockwise: true)
circleLayer.path = circularPath.cgPath
circleLayer.fillColor = UIColor.clear.cgColor
circleLayer.strokeColor = UIColor.lightGray.cgColor
circleLayer.lineCap = .round
circleLayer.lineWidth = 20.0 //for thicker circle compared to progressLayer
progressLayer.path = circularPath.cgPath
progressLayer.fillColor = UIColor.clear.cgColor
progressLayer.strokeColor = UIColor.blue.cgColor
progressLayer.lineCap = .round
progressLayer.lineWidth = 10.0
progressLayer.strokeEnd = 0
view.addSublayer(circleLayer)
view.addSublayer(progressLayer)
let progressAnimation = CABasicAnimation(keyPath: "strokeEnd")
progressAnimation.toValue = 1
progressAnimation.fillMode = .forwards
progressAnimation.isRemovedOnCompletion = false
progressLayer.add(progressAnimation, forKey: "progressAnim")
}
Happy Coding!
Hope this answer helps :]

Resources