Animate roundedrect with CoreAnimation - swift

Guys I have the following code to produce a bezierpath
firstLine.path = UIBezierPath(roundedRect: CGRect(x: frameSize.width / 2.2, y: frameSize.height / 2.6, width: 4, height: 4), cornerRadius: 10).cgPath
I try to animate the firstLine by moving it to the right of the screen by x value and back to the original position. But I don't know what to use for the keyPath in the animation.
let animation = CABasicAnimation(keyPath: "??")
animation.toValue = // this should be the position to move?
animation.duration = 0.5 // duration of the animation
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut) // animation curve is Ease Out
animation.fillMode = kCAFillModeBoth // keep to value after finishing
animation.isRemovedOnCompletion = false // don't remove after finishing
animation.autoreverses = true
animation.repeatCount = HUGE
Can someone help me out on this :)

From: https://developer.apple.com/reference/quartzcore/cabasicanimation
You create an instance of CABasicAnimation using the inherited init(keyPath:) method, specifying the key path of the property to be animated in the render tree.
Example of what you want:
let animation = CABasicAnimation(keyPath: "position")
animation.fromValue = [startX, startY]
animation.toValue = [destinationX, destinationY]

Related

How to achieve this animation with multiple CAShapeLayer

I'm having a lot of trouble exporting a video with animated layers.
The final goal is to export to a video (that's why I need this done with CALayers) a moving CAShapeLayer around the screen and a line following its trace.
First of all, I have an array of CGPoint with all the coordinate points which a CAShapeLayer should use to animate itself. To create the animation I'm using a CAKeyframeAnimation.
let values = [CGPoint(x: 50, y: 100),
CGPoint(x: 100, y: 150)] //Example values
let animation = CAKeyframeAnimation()
animation.keyPath = "position"
animation.values = values
animation.duration = videoLength
With this, I'm able to show the CALayer moving around the screen. It's 1 value per frame of the video, it's a 1:1 value.
The real problem comes with adding a line that shows the path of the CAShapeLayer.
What I think I might do, is to create lines with UIBezierPath, animate them and add each line
lines.forEach { (line) in
let path = UIBezierPath()
let shapeLayer = CAShapeLayer()
for (index, point) in line.points.enumerated() {
if index == 0 {
path.move(to: point)
} else {
path.addLine(to: point)
}
}
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = line.color.cgColor
shapeLayer.lineWidth = 4
shapeLayer.position = animatedLayer.position
shapeLayer.path = path.cgPath
shapeLayer.opacity = 0
shapeLayers.append(shapeLayer)
}
And to animate the lines...
var timeOffset: Double = 0
let eachLineTime = totalTime / Double(shapeLayers.count)
for shapeLayer in shapeLayers {
CATransaction.begin()
mainLayer.addSublayer(shapeLayer)
let strokeAnimation = CABasicAnimation(keyPath: "strokeEnd")
strokeAnimation.fromValue = 0
strokeAnimation.toValue = 1
strokeAnimation.duration = eachLineTime
strokeAnimation.beginTime = CACurrentMediaTime() + timeOffset
let opacityAnimation = CABasicAnimation(keyPath: "opacity")
opacityAnimation.fromValue = 0
opacityAnimation.toValue = 1
opacityAnimation.duration = eachLineTime
opacityAnimation.beginTime = CACurrentMediaTime() + timeOffset
CATransaction.setCompletionBlock {
shapeLayer.strokeColor = UIColor.orange.cgColor
shapeLayer.opacity = 1
}
shapeLayer.add(strokeAnimation, forKey: nil)
shapeLayer.add(opacityAnimation, forKey: nil)
timeOffset += eachLineTime
CATransaction.commit()
}
I need them to be different lines because I want to change the color of each line when it finishes each animation.
I'm getting the next output in Swift Playgrounds
But I'm not getting anything when I export the video in the app.
How can I simply add a trail of the first CAShapeLayer to see the (square in this case) moving path?
How can I move the CABasicAnimation with the same timing of the CAKeyframeAnimation?

Change Color of an Animated UIBezierPath at a Certain Point

I am trying to create a circular progress bar in Swift 4 using a CAShapeLayer and animated UIBezierPath. This works fine but I would like the circle to change it's strokeColor once the animation reaches a certain value.
For example: Once the circle is 75% drawn I want to switch the strokeColor from UIColor.black.cgColor to UIColor.red.cgColor.
My code for the circle and the "progress" animation looks like this:
let circleLayer = CAShapeLayer()
// set initial strokeColor:
circleLayer.strokeColor = UIColor.black.cgColor
circleLayer.path = UIBezierPath([...]).cgPath
// animate the circle:
let animation = CABasicAnimation()
animation.keyPath = #keyPath(CAShapeLayer.strokeEnd)
animation.fromValue = 0.0
animation.toValue = 1
animation.duration = 10
animation.isAdditive = true
animation.fillMode = .forwards
circleLayer.add(animation, forKey: "strokeEnd")
I know that is also possible to create a CABasicAnimation for the strokeColor keypath and set the fromValue and toValue to UIColors to get the strokeColor to slowly change. But this is like a transition over time which is not exactly what I want.
Update 1:
Based on Mihai Fratu's answer I was able to solve my problem. For future reference I want to add a minimal Swift 4 code example:
// Create the layer with the circle path (UIBezierPath)
let circlePathLayer = CAShapeLayer()
circlePathLayer.path = UIBezierPath([...]).cgPath
circlePathLayer.strokeEnd = 0.0
circlePathLayer.strokeColor = UIColor.black.cgColor
circlePathLayer.fillColor = UIColor.clear.cgColor
self.layer.addSublayer(circlePathLayer)
// Create animation to animate the progress (circle slowly draws)
let progressAnimation = CABasicAnimation()
progressAnimation.keyPath = #keyPath(CAShapeLayer.strokeEnd)
progressAnimation.fromValue = 0.0
progressAnimation.toValue = 1
// Create animation to change the color
let colorAnimation = CABasicAnimation()
colorAnimation.keyPath = #keyPath(CAShapeLayer.strokeColor)
colorAnimation.fromValue = UIColor.black.cgColor
colorAnimation.toValue = UIColor.red.cgColor
colorAnimation.beginTime = 3.75 // Since your total animation is 10s long, 75% is 7.5s - play with this if you need something else
colorAnimation.duration = 0.001 // make this really small - this way you "hide" the transition
colorAnimation.fillMode = .forwards
// Group animations together
let progressAndColorAnimation = CAAnimationGroup()
progressAndColorAnimation.animations = [progressAnimation, colorAnimation]
progressAndColorAnimation.duration = 5
// Add animations to the layer
circlePathLayer.add(progressAndColorAnimation, forKey: "strokeEndAndColor")
If I understood your question right this should do what you are after. Please bare in mind that it's not tested at all:
let circleLayer = CAShapeLayer()
// set initial strokeColor:
circleLayer.strokeColor = UIColor.black.cgColor
circleLayer.path = UIBezierPath([...]).cgPath
// animate the circle:
let animation = CABasicAnimation()
animation.keyPath = #keyPath(CAShapeLayer.strokeEnd)
animation.fromValue = 0.0
animation.toValue = 1
animation.beginTime = 0 // Being part of an animation group this is relative to the animation group start time
animation.duration = 10
animation.isAdditive = true
animation.fillMode = .forwards
// animate the circle color:
let colorAnimation = CABasicAnimation()
colorAnimation.keyPath = #keyPath(CAShapeLayer.strokeColor)
colorAnimation.fromValue = UIColor.black.cgColor
colorAnimation.toValue = UIColor.black.red
colorAnimation.beginTime = 7.5 // Since your total animation is 10s long, 75% is 7.5s - play with this if you need something else
colorAnimation.duration = 0.0001 // make this really small - this way you "hide" the transition
colorAnimation.isAdditive = true
colorAnimation.fillMode = .forwards
let sizeAndColorAnimation = CAAnimationGroup()
sizeAndColorAnimation.animations = [animation, colorAnimation]
sizeAndColorAnimation.duration = 10
circleLayer.add(sizeAndColorAnimation, forKey: "strokeEndAndColor")

Circle scale animation works a bit weird

I am animating my GMSMarker so that it pulse once in couple seconds.
func addWave()
{
// circleView.layer.cornerRadius = size / 2
//Scale
let scaleAnimation = CABasicAnimation(keyPath: "transform.scale")
scaleAnimation.fromValue = 1
scaleAnimation.toValue = zoom
//Opacity
let alphaAnimation = CABasicAnimation(keyPath: "opacity")
alphaAnimation.toValue = 0.0
//Corner radius
// let cornerRadiusAnimation = CABasicAnimation(keyPath: "cornerRadius")
// cornerRadiusAnimation.fromValue = size / 2
// cornerRadiusAnimation.toValue = (size * zoom)/2
//Animation Group
let animations: [CAAnimation] = [scaleAnimation, alphaAnimation]
let animationGroup = CAAnimationGroup()
animationGroup.duration = duration
animationGroup.animations = animations
animationGroup.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
animationGroup.repeatCount = 1
animationGroup.fillMode = kCAFillModeForwards
animationGroup.isRemovedOnCompletion = false
circleView.layer.add(animationGroup, forKey: "group")
}
The result looks like this:
And if I uncomment Corner radius section it looks like this:
So I need an advice.
Based on my observations, I think it must be a issue of wrong path of your CAShapeLayer hence the masking of the circle.
I just wrote a radar animation, I hope it might help you.
let scaleAnimation = CABasicAnimation(keyPath: "transform.scale")
scaleAnimation.fromValue = 0
scaleAnimation.toValue = 1
let alphaAnimation = CABasicAnimation(keyPath: "opacity")
alphaAnimation.fromValue = 1
alphaAnimation.toValue = 0
let animations = CAAnimationGroup()
animations.duration = 0.8
animations.repeatCount = Float.infinity
animations.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
animations.animations = [scaleAnimation, alphaAnimation]
circleView.layer.add(animations, forKey: "animations")
What I did was, I used two CAShapeLayer (one being the orange marker and other is the rader layer at the back of marker). The animations were applied on the radar layer.
radarLayer = CAShapeLayer()
radarLayer.frame = CGRect(x: 0, y: 0, width: self.frame.size.width, height: self.frame.size.height);
radarLayer.path = UIBezierPath(rect: radarLayer.frame).cgPath
radarLayer.fillColor = UIColor.orange.cgColor
radarLayer.position = CGPoint(x: self.frame.size.width/2, y: self.frame.size.height/2)
radarLayer.cornerRadius = radarLayer.frame.size.width/2
radarLayer.masksToBounds = true
radarLayer.opacity = 0
self.layer.addSublayer(radarLayer)
circleLayer = CAShapeLayer()
circleLayer.frame = CGRect(x: 0, y: 0, width: 16, height: 16);
circleLayer.path = UIBezierPath(rect: circleLayer.frame).cgPath
circleLayer.fillColor = UIColor.orange.cgColor
circleLayer.position = CGPoint(x: self.frame.size.width/2, y: self.frame.size.height/2)
circleLayer.cornerRadius = circleLayer.frame.size.width/2
circleLayer.masksToBounds = true
self.layer.addSublayer(circleLayer)
PS. I'm new to Swift, so enlighten me if I'm wrong somewhere :)
Example : http://i.imgur.com/v2jFWgw.jpg
The following in an excerpt from Google documentation on Markers:
The view behaves as if clipsToBounds is set to YES, regardless of its actual value. You can apply transforms that work outside the bounds, but the object you draw must be within the bounds of the object. All transforms/shifts are monitored and applied. In short: subviews must be contained within the view.
The consequence of that, at least for my case, was exactly the behaviour mentioned in the question: a squared circle animation. The image I wanted to add inside the marker, at the end of the animation, was bigger than the container, so once added to the marker, it was clipped to the bounds of it.
The following is what I did:
public var pulseImageView: UIImageView = {
let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 300, height: 300))
imageView.image = PaletteElements.pulseLocation.value
imageView.contentMode = .center
let pulseAnimation = CABasicAnimation(keyPath: "transform.scale.xy")
pulseAnimation.repeatCount = Float.infinity
pulseAnimation.fromValue = 0
pulseAnimation.toValue = 2.0
pulseAnimation.isRemovedOnCompletion = false
let fadeOutAnimation = CABasicAnimation(keyPath: "opacity")
fadeOutAnimation.duration = 2.5
fadeOutAnimation.fromValue = 1.0
fadeOutAnimation.toValue = 0
fadeOutAnimation.repeatCount = Float.infinity
let animationGroup = CAAnimationGroup()
animationGroup.duration = 2.5
animationGroup.animations = [pulseAnimation, fadeOutAnimation]
animationGroup.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
animationGroup.repeatCount = .greatestFiniteMagnitude
animationGroup.fillMode = CAMediaTimingFillMode.forwards
animationGroup.isRemovedOnCompletion = false
imageView.layer.add(animationGroup, forKey: "pulse")
return imageView
}()
I defined a var inside my helper class so I can retrieve the image whenever I need it. My original image, the pulse image, was 116x116, so I created the imageView as 300x300 with a contentMode = .center, so the small image was in the center and not stretched. I chose 300x300 because my animation scaled up the pulse image until a 2x its initial value (116x2), so I made room for the entire animation to be performed.
Finally I added it to as a Marker to the map:
let pulseMarker = GMSMarker(position: userLocation.coordinate)
pulseMarker.iconView = pulseImageView
pulseMarker.groundAnchor = CGPoint(x: 0.5, y: 0.5)
pulseMarker.map = googleMapView
Hope it can help.

Adding CAShapeLayer to UIButton Not Working

I am working on a Swift project and creating a circle around a UIButton, using CAShapeLayer and creating a circular UIBezeir path.The problem is that if I add this CAShapLayer as a sublayer to UIbutton,It does not work. However adding this sublayer on UiView creates the circle.
Below is my code.
let ovalPath = UIBezierPath(arcCenter: lockButton.center, radius:
CGFloat(lockButton.frame.size.width/2), startAngle: CGFloat(startAngle), endAngle: CGFloat(sentAngel), clockwise: true)
UIColor.grayColor().setFill()
ovalPath.fill()
let circleLayer = CAShapeLayer()
circleLayer.path = ovalPath.CGPath
view.layer.addSublayer(circleLayer)
circleLayer.strokeColor = UIColor.blueColor().CGColor
circleLayer.fillColor = UIColor.clearColor().CGColor
circleLayer.lineWidth = 10.0
let animation = CABasicAnimation(keyPath: "strokeEnd")
// Set the animation duration appropriately
animation.duration = 0.5
// Animate from 0 (no circle) to 1 (full circle)
animation.fromValue = 0
animation.toValue = 1
// Do a linear animation (i.e. the speed of the animation stays the same)
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
// Set the circleLayer's strokeEnd property to 1.0 now so that it's the
// right value when the animation ends.
circleLayer.strokeEnd = 3.0
// Do the actual animation
circleLayer.addAnimation(animation, forKey: "animateCircle")
Can anyone suggest me, if we can add CAShapeLayer to UIButton?
Thanks
One very obvious problem with your code is that your circleLayer has no frame. It's kind of amazing that anything appears at all. You need to set the frame of the circleLayer so that it has size and so that it is positioned correctly with respect to its superlayer.
To resolve this problem you need to remove UIButton background color and set it on the default value.

CAShapelayer Duplicates

I'm trying to add a CAShapelayer once every 20ms to given x and y coordinates. I would like the shape to fade away over a second (like a tracer). The function I have created works, the shape is created in the correct location and fades away. But I am getting extra shapes left behind cluttering up the screen.
func shadowBall (x: CGFloat, y: CGFloat){
let xpos : CGFloat = ((self.frame.width/2) + x)
let ypos : CGFloat = ((self.frame.height/2) + y)
let shadowBall = CAShapeLayer()
let shadowBalllRadius :CGFloat = 4
let shadowBallPath : UIBezierPath = UIBezierPath(ovalInRect: CGRect(x: xpos, y: ypos, width: CGFloat(shadowBalllRadius*2), height: CGFloat(shadowBalllRadius*2)))
shadowBall.path = shadowBallPath.CGPath
shadowBall.fillColor = UIColor.clearColor().CGColor
shadowBall.strokeColor = UIColor.whiteColor().CGColor
shadowBall.lineWidth = 0.5
let animation: CABasicAnimation = CABasicAnimation(keyPath: "strokeColor")
animation.fromValue = UIColor.whiteColor().CGColor
animation.toValue = UIColor.clearColor().CGColor
animation.duration = 1.0;
animation.repeatCount = 0;
animation.removedOnCompletion = true
animation.additive = false
self.layer.addSublayer(shadowBall)
shadowBall.addAnimation(animation, forKey: "strokeColor")
}
The problem is that when the animation finishes, it restores the strokeColor to the original color. You should really set the strokeColor of the original shape layer to be clearColor(), that way, when you finish animating from whiteColor() to clearColor(), it will remain at clearColor().
You can also set the layer's fillMode to kCAFillModeForwards and set removedOnCompletion to false and that will have the layer preserve its "end of animation" state. But I personally would just set the strokeColor as outlined above, as using removedOnCompletion of true interferes with animationDidStop (see below).
Also, I might suggest that you also remove the layer once it's done with the animation so it doesn't continue to consume memory although it's no longer visible.
func shadowBall (x: CGFloat, y: CGFloat) {
let xpos: CGFloat = ((self.frame.width/2) + x)
let ypos: CGFloat = ((self.frame.height/2) + y)
let shadowBall = CAShapeLayer()
let shadowBalllRadius: CGFloat = 4
let shadowBallPath = UIBezierPath(ovalInRect: CGRect(x: xpos, y: ypos, width: CGFloat(shadowBalllRadius*2), height: CGFloat(shadowBalllRadius*2)))
shadowBall.path = shadowBallPath.CGPath
shadowBall.fillColor = UIColor.clearColor().CGColor
shadowBall.strokeColor = UIColor.clearColor().CGColor
shadowBall.lineWidth = 0.1
let animation = CABasicAnimation(keyPath: "strokeColor")
animation.fromValue = UIColor.whiteColor().CGColor
animation.toValue = UIColor.clearColor().CGColor
animation.duration = 1.0
animation.repeatCount = 0
animation.removedOnCompletion = true
animation.additive = false
animation.delegate = self
animation.setValue(shadowBall, forKey: "animationLayer")
self.layer.addSublayer(shadowBall)
shadowBall.addAnimation(animation, forKey: "strokeColor")
}
override func animationDidStop(anim: CAAnimation, finished flag: Bool) {
if let layer = anim.valueForKey("animationLayer") as? CALayer {
layer.removeFromSuperlayer()
}
}
See How to remove a CALayer-object from animationDidStop?