How do I set an endpoint for a CAKeyFrameAnimation? - swift

I am using an arc for a progress bar.
There is a track layer that the progress indicator moves along and I also have a circle that moves with the progress bar for added emphasis. Both the progress bar and the circle move correctly along the path but when the progress is complete, instead of stopping at the end of the path, the circle jumps back to the beginning of the progress path.
screen recording of behavior
The code:
class ViewController: UIViewController {
let shapeLayer = CAShapeLayer()
let trackLayer = CAShapeLayer()
let dotLayer: CALayer = CALayer()
let seconds = 10.0
// MARK: - Properties
private var progressBarView = UIView()
override func viewDidLoad() {
super.viewDidLoad()
title = "Making Progress"
view.backgroundColor = .systemTeal
// determine the value of Center
let center = view.center
let circularPath = UIBezierPath(arcCenter: center, radius: 100, startAngle: 2.7, endAngle: 0.45, clockwise: true) // these values give us a complete circle
trackLayer.path = circularPath.cgPath
trackLayer.strokeColor = UIColor.lightGray.cgColor
trackLayer.lineWidth = 10
trackLayer.fillColor = UIColor.clear.cgColor
trackLayer.lineCap = .round
view.layer.addSublayer(trackLayer)
shapeLayer.path = circularPath.cgPath
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.lineWidth = 10
shapeLayer.strokeEnd = 0
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineCap = .round
view.layer.addSublayer(shapeLayer)
view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTap )))
}
#objc private func handleTap() {
print("Attempting to animate stroke")
let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
basicAnimation.toValue = 1
basicAnimation.duration = seconds
basicAnimation.fillMode = CAMediaTimingFillMode.forwards
basicAnimation.isRemovedOnCompletion = false
shapeLayer.add(basicAnimation, forKey: "anyKeyWillDo")
// circle image
let circleNobImage = UIImage(named: "whiteCircle.png")!
dotLayer.contents = circleNobImage.cgImage
dotLayer.bounds = CGRect(x: 0.0, y: 0.0, width: circleNobImage.size.width, height: circleNobImage.size.height)
view.layer.addSublayer(dotLayer)
dotLayer.position = CGPoint(x: 105, y: 460)
dotLayer.opacity = 1
dotAnimation()
}
func dotAnimation() {
let dotAnimation = CAKeyframeAnimation(keyPath: "position")
dotAnimation.path = trackLayer.path
dotAnimation.calculationMode = .paced
let dotAnimationGroup = CAAnimationGroup()
dotAnimationGroup.duration = seconds
dotAnimation.autoreverses = false
dotAnimationGroup.animations = [dotAnimation]
dotLayer.add(dotAnimationGroup, forKey: nil)
}
}
I need to get the circle to stop at the end of the arc with the progress bar. How is this done?

Also set dotAnimationGroup.fillMode and dotAnimationGroup.isRemovedOnCompletion
dotAnimationGroup.fillMode = .forwards
dotAnimationGroup.isRemovedOnCompletion = false

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.

Gradient Arc View Not Showing Correctly

i have a custom view that is a 85% of a circle ( Just Stroke ) with Gradient color . here is my code :
class ProfileCircle:UIView
{
override func draw(_ rect: CGRect)
{
let desiredLineWidth:CGFloat = 4 // your desired value
let arcCenter = CGPoint(x: self.bounds.midX, y: self.bounds.midY)
let radius = self.bounds.midX
let circlePath = UIBezierPath(
arcCenter: arcCenter,
radius: radius,
startAngle: CGFloat(0),
endAngle:CGFloat((.pi * 2)*0.85),
clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.lineCap = .round
shapeLayer.path = circlePath.cgPath
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.lineWidth = desiredLineWidth
let gradient = CAGradientLayer()
gradient.frame = circlePath.bounds
gradient.colors = [UIColor.colorPrimary.cgColor, UIColor.colorSecondary.cgColor]
//layer.addSublayer(shapeLayer)
gradient.mask = shapeLayer
layer.addSublayer(gradient)
}
}
but the edges are clipped
i have tried a lot but i cant fix it .
one more thing is that when i change the radius manually the view wont be centered
Just two changes:
let radius = self.bounds.midX - desiredLineWidth
and
gradient.frame = self.bounds

How to add animation inside a UICollectionViewCell

I'm attempting to add a circular loading animation inside of a UICollectionViewCell. I've added the exact same code to a normal UIViewController and it worked successfully but I don't get any results on a UICollectionViewCell.
Ideally I'd like to have the animation start when the cell becomes visible.
let shapeLayer = CAShapeLayer()
let trackLayer = CAShapeLayer()
func setupBezierPaths() {
let center = self.center
let circularPath = UIBezierPath(arcCenter: center, radius: 100, startAngle: -CGFloat.pi / 2, endAngle: 2 * CGFloat.pi, clockwise: true)
// Create a track layer
trackLayer.path = circularPath.cgPath
trackLayer.strokeColor = UIColor.lightGray.cgColor
trackLayer.lineWidth = 10
trackLayer.lineCap = CAShapeLayerLineCap.round
trackLayer.fillColor = UIColor.clear.cgColor
layer.addSublayer(trackLayer)
// Create a layer that is animated
shapeLayer.path = circularPath.cgPath
shapeLayer.strokeColor = UIColor.green.cgColor
shapeLayer.lineWidth = 10
shapeLayer.strokeEnd = 0
shapeLayer.lineCap = CAShapeLayerLineCap.round
shapeLayer.fillColor = UIColor.clear.cgColor
layer.addSublayer(shapeLayer)
}
For the UICollectionViewCell, I'm not sure where to put the actual animation code. I've tried it inside the init but the track layer doesn't even show up. Here is the code for the animation:
let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
basicAnimation.toValue = 1
basicAnimation.duration = 1
basicAnimation.fillMode = CAMediaTimingFillMode.forwards
basicAnimation.isRemovedOnCompletion = false
shapeLayer.add(basicAnimation, forKey: "simpleAnimation")

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!)
}
}

Animating a UIView's mask (larger circle CAShapeLayer to smaller)

I have a UIView class that I add to my main view that blocks everything out except for a circle shape. Now I'm trying to animate the circle. I decided to use CABasicAnimation to do this, but the circle stays large and doesn't animate. I'm not sure if I'm missing something or what I'm trying to do is impossible.
class SpotlightOverlay: UIView {
let overlayView = UIView()
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.clear
}
init(frame: CGRect,
xOffset: CGFloat,
yOffset: CGFloat,
radius: CGFloat) {
super.init(frame: frame)
overlayView.frame = frame
overlayView.backgroundColor = UIColor.black.withAlphaComponent(0.7)
let path = CGMutablePath()
path.addArc(center: CGPoint(x: xOffset, y: yOffset),
radius: radius*5,
startAngle: 0.0,
endAngle: 2.0 * .pi,
clockwise: false)
path.addRect(CGRect(origin: .zero, size: overlayView.frame.size))
let path2 = CGMutablePath()
path2.addArc(center: CGPoint(x: xOffset, y: yOffset),
radius: radius,
startAngle: 0.0,
endAngle: 2.0 * .pi,
clockwise: false)
path2.addRect(CGRect(origin: .zero, size: overlayView.frame.size))
let anim = CABasicAnimation(keyPath: "path")
anim.fromValue = path
anim.toValue = path2
anim.duration = 6.0
anim.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
let maskLayer = CAShapeLayer()
maskLayer.backgroundColor = UIColor.black.cgColor
maskLayer.add(anim, forKey: nil)
CATransaction.begin()
CATransaction.setDisableActions(true)
maskLayer.path = path
CATransaction.commit()
maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
overlayView.layer.mask = maskLayer
overlayView.clipsToBounds = true
self.addSubview(overlayView)
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Instead of using a CGMutablePath, I achieved what I wanted with a UIBezierPath. More specifically, it appears as a hole in the view that can animate.
import UIKit
class SpotlightOverlay: UIView {
let overlayView = UIView()
let maskLayer = CAShapeLayer()
var initXOffset:CGFloat = 0
var initYOffset:CGFloat = 0
var initDiameter:CGFloat = 0
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.clear
}
init(frame: CGRect,
xOffset: CGFloat,
yOffset: CGFloat,
startDiameter: CGFloat,
endDiameter:CGFloat,
duration:Double) {
super.init(frame: frame)
self.isUserInteractionEnabled = false
self.overlayView.isUserInteractionEnabled = false
overlayView.frame = frame
overlayView.backgroundColor = UIColor.black.withAlphaComponent(0.7)
let padding:CGFloat = 50
let startDiameter = startDiameter + padding
let endDiameter = endDiameter + padding
initXOffset = xOffset
initYOffset = yOffset
initDiameter = endDiameter
let ovalFrame1 = CGRect(x:xOffset-(startDiameter/2),
y:yOffset-(startDiameter/2),
width:startDiameter,
height:startDiameter)
let ovalFrame2 = CGRect(x:xOffset-(endDiameter/2),
y:yOffset-(endDiameter/2),
width:endDiameter,
height:endDiameter)
let path = UIBezierPath(ovalIn: ovalFrame1)
let path2 = UIBezierPath(ovalIn: ovalFrame2)
maskLayer.backgroundColor = UIColor.black.cgColor
path.append(UIBezierPath(rect: self.bounds))
path2.append(UIBezierPath(rect: self.bounds))
maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
CATransaction.begin()
CATransaction.setDisableActions(true)
maskLayer.path = path.cgPath
CATransaction.commit()
let anim = CABasicAnimation(keyPath: "path")
anim.fromValue = path.cgPath
anim.toValue = path2.cgPath
anim.duration = duration
anim.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
anim.fillMode = CAMediaTimingFillMode.forwards
anim.isRemovedOnCompletion = false
maskLayer.add(anim, forKey: nil)
overlayView.layer.mask = maskLayer
overlayView.clipsToBounds = true
self.addSubview(overlayView)
}
func animateTo(xOffset: CGFloat,
yOffset: CGFloat,
endDiameter:CGFloat,
duration:Double) {
let padding:CGFloat = 50
let startDiameter = initDiameter
let endDiameter = endDiameter + padding
let ovalFrame1 = CGRect(x:initXOffset-(startDiameter/2),
y:initYOffset-(startDiameter/2),
width:startDiameter,
height:startDiameter)
let ovalFrame2 = CGRect(x:xOffset-(endDiameter/2),
y:yOffset-(endDiameter/2),
width:endDiameter,
height:endDiameter)
initXOffset = xOffset
initYOffset = yOffset
let path = UIBezierPath(ovalIn: ovalFrame1)
let path2 = UIBezierPath(ovalIn: ovalFrame2)
path.append(UIBezierPath(rect: self.bounds))
path2.append(UIBezierPath(rect: self.bounds))
CATransaction.begin()
CATransaction.setDisableActions(true)
maskLayer.path = path.cgPath
CATransaction.commit()
let anim = CABasicAnimation(keyPath: "path")
anim.fromValue = path.cgPath
anim.toValue = path2.cgPath
anim.duration = duration
anim.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
anim.fillMode = CAMediaTimingFillMode.forwards
anim.isRemovedOnCompletion = false
maskLayer.add(anim, forKey: nil)
initDiameter = endDiameter
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
First, initialize the class and then call the animateTo function any time you want to animate the hole moving.