I am implementing a simple activity indicator using Swift and Core Animation. The core animation loop consists of just two animations. When I add them to the layer directly, they work perfectly. When I add them as a CAAnimationGroup, nothing is happening at all. I am utterly confused by this behaviour, and I've already checked all questions on stackoverflow about CAAnimationGroup, all tutorials on the web, and read official docs many times. I can't figure out what's going on. Please help.
Animations:
let anim1 = CABasicAnimation(keyPath: "strokeEnd")
anim1.fromValue = 0
anim1.toValue = 1.0
anim1.duration = 2.0
anim1.beginTime = CACurrentMediaTime()
let anim2 = CABasicAnimation(keyPath:"strokeStart")
anim2.fromValue = 0
anim2.toValue = 1.0
anim2.duration = 2.0
anim2.beginTime = CACurrentMediaTime() + 2.0
This works perfectly as expected:
shapeLayer.addAnimation(anim1, forKey:nil)
shapeLayer.addAnimation(anim2, forKey:nil)
This does nothing whatsoever to my layer:
let group = CAAnimationGroup()
group.animations = [anim1, anim2]
group.duration = 4.0
shapeLayer.addAnimation(group, forKey: nil)
I made a short demo snippet to be used in Playground: https://gist.github.com/anonymous/6021295eab4e00b813ce. Please see for yourself and help me solve this. (In case you're not sure how to use Playground for prototyping animations: http://possiblemobile.com/2015/03/prototyping-uiview-animations-swift-playground/)
#MDB983's answer looks like it would work, but doesn't explain why.
When you add an animation directly to a layer, it's begin time is based on the current media time.
When you add animations to an animation group, their "local time" is the animation group. A begin time of 0 in an animation group is the beginning of the group, and a beginTime of 1.0 is 1 second into the total animation group, etc. When you add animations to an animation group change your begin times to be zero-based, not starting from CACurrentMediaTime().
Try it this way, seemingly CACurrentMediaTime() + 2.0 is causing an issue
import UIKit
import XCPlayground
let view = UIView(frame: CGRect(x: 0, y: 0, width: 500, height: 500))
let shapeLayer = CAShapeLayer()
let size:CGFloat = 52
let rect = CGRect(x: view.bounds.midX - size/2, y: view.bounds.midY-size/2, width: size, height: size)
shapeLayer.path = UIBezierPath(ovalInRect: rect).CGPath
shapeLayer.fillColor = UIColor.clearColor().CGColor
shapeLayer.strokeColor = UIColor.redColor().CGColor
shapeLayer.lineWidth = 3
shapeLayer.strokeStart = 0
shapeLayer.strokeEnd = 0
view.layer.addSublayer(shapeLayer)
XCPShowView("view", view)
let beginTime = CACurrentMediaTime()
let anim1 = CABasicAnimation(keyPath: "strokeEnd")
anim1.fromValue = 0
anim1.toValue = 1
anim1.duration = 2
let anim2 = CABasicAnimation(keyPath:"strokeEnd")
anim2.fromValue = 1
anim2.toValue = 0
anim2.duration = 2
anim2.beginTime = 2
let group = CAAnimationGroup()
group.duration = 4.0
group.removedOnCompletion = true
group.animations = [anim1, anim2]
//shapeLayer.addAnimation(anim1, forKey:nil)
//shapeLayer.addAnimation(anim2, forKey:nil)
shapeLayer.addAnimation(group, forKey: nil)
I know this is an old question but I was doing the same with animation group in swift 4. What I missed is specifying the beginTime for the CAAnimationGroup itself (I assumed that the child animations would follow their beginTime). By default, beginTime is 0.0 for CAAnimationGroup
let group = CAAnimationGroup()
group.beginTime = CACurrentMediaTime() + delay
group.duration = 4.0
group.removedOnCompletion = true
group.animations = [anim1, anim2]
Related
Thanks to #matt, I learned a while back that it is possible to animate multiple layers' properties in a CAAnimationGroup. The trick is to add name the child layer(s).
See Matt's answer in this thread for a discussion of the technique, plus a working example I wrote to test it out.
You then add the animation group to the parent layer, and in the animations that affect the sublayers of the parent layer, you create those sublayer animations using a keyPath value that references the child layer as a named sublayer of the parent layer.
So if I have CAShapeLayer parentLayer, that also has a sublayer named "sublayer",
let parentLayer = CAShapeLayer()
let sublayer = CAShapeLayer()
sublayer.name = "sublayer"
parentLayer.addSublayer(sublayer)
And I want to create an animation group that animates properties of the parent layer and the sublayer at the same time, that code might look like this:
let animationGroup = CAAnimationGroup()
animationGroup.duration = animationDuration
let parentLayerAnimation = CABasicAnimation(keyPath: "path")
parentLayerAnimation.duration = animationDuration
parentLayerAnimation.fromValue = parentLayer.path
parentLayerAnimation.toValue = newPath
let sublayerAnimation = CABasicAnimation(keyPath: "sublayers.sublayer.path")
sublayerAnimation.duration = animationDuration
sublayerAnimation.fromValue = subLayer.path
sublayerAnimation.toValue = newSublayerPath
let animationGroup = CAAnimationGroup()
animationGroup.duration = animationDuration
animationGroup.animations = [parentLayerAnimation, sublayerAnimation]
parentLayer.add(animationGroup, forKey: nil)
The code above does not work.
The problem appears to be specifically with trying to animate the path of the sublayer. If I instead animate the sublayer's rotation, using a code like this:
let sublayerRotationAnimation = CABasicAnimation(keyPath: "sublayers.sublayer.transform.rotation.z")
sublayerRotationAnimation.duration = animationDuration
sublayerRotationAnimation.fromValue = 0
sublayerRotationAnimation.toValue = CGFloat.pi
And add the sublayerRotationAnimation to the animation group, that works.
The problem seems to occur if I try to animate the sublayer's path. Animating other properties works.
(And if I change the key path of the sublayer's animation to just "path" and add the animation directly to the sublayer, that works too.)
The effect I am after is this:
(The black caret that animates into an arc is the parent layer, and the red lines with the dots are the sublayer bezierIllustrationLayer.)
I have a branch of a working project on Github that illustrates this problem:
Github project branch with animation group that doesn't work
The main branch of the project simply submits separate animations to the layer and the sublayer, and that works.
The code in that branch that attempts to add an animation group is in the file CaretToArcView.swift:
let pathAnimation = CABasicAnimation(keyPath: "path")
pathAnimation.duration = animationDuration
pathAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
pathAnimation.fromValue = layer.path
pathAnimation.toValue = path.cgPath
// This animation does not work when added to an animation group applied to the parent layer of the bezierIllustrationLayer
let bezierPathAnimation = CABasicAnimation(keyPath: "sublayers.bezierillustrationlayer.path")
bezierPathAnimation.duration = animationDuration
bezierPathAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
bezierPathAnimation.fromValue = bezierIllustrationLayer.path
bezierPathAnimation.toValue = illustrationPath.cgPath
//If I add this rotation animation to the animation group, it works
//let rotationAnimation = CABasicAnimation(keyPath: "sublayers.bezierillustrationlayer.transform.rotation.z")
//rotationAnimation.duration = 1.0
//rotationAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
//rotationAnimation.fromValue = 0
//rotationAnimation.toValue = CGFloat.pi * 2
let animationGroup = CAAnimationGroup()
animationGroup.duration = animationDuration
animationGroup.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
animationGroup.animations = [pathAnimation, bezierPathAnimation]
layer.add(animationGroup, forKey: nil)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
layer.path = path.cgPath
self.bezierIllustrationLayer.path = illustrationPath.cgPath
}
As far as I can tell, this is a bug in CAAnimationGroup. The same animation works if I apply it directly to the sublayer (after changing the keyPath appropriately). An animation of a different property like the sublayer's transform.rotation.z works in an animation group. It's just animating the sublayer's path that fails.
Has anybody else encountered this, and do you have a solution? Matt? I think you're the reigning expert on CAAnimation here. Can you help?
I want to animate shadow properties for a view. Initially I tried:
func animateIn(delay: TimeInterval = 0) {
UIView.animate(withDuration: 0.2, delay: delay, options: .allowUserInteraction, animations: {
self.shadowView.layer.shadowColor = UIColor.black.cgColor
self.shadowView.layer.shadowOffset = CGSize(width: 15, height: 15)
self.shadowView.layer.shadowRadius = 20
self.shadowView.layer.shadowOpacity = 0.2
self.layoutIfNeeded()
}, completion: nil)
}
which doesnt work given that UIView.animate doesnt work with layers.
I tested the following post (How to animate layer shadowOpacity?), but it only specifies how to animate the shadow Opacity. Is there any way to animate all my shadow properties like I would with an animation blocl?
You are very close, you just create multiple CABasicAnimations for all the property changes and then you can use CAAnimationGroup
So let say you have 2 CABasicAnimation animations like:
let shadownOpacityAnimation = CABasicAnimation(keyPath: "shadowOpacity")
...
let shadowRadiusAnimation = CABasicAnimation(keyPath: "shadowRadius")
...
Then you can add them using CAAnimationGroup
let group = CAAnimationGroup()
group.duration = 0.2
group.repeatCount = 1
group.autoreverses = false
group.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
group.animations = [shadownOpacityAnimation, shadowRadiusAnimation]
layer.add(group, forKey: "allAnimations")
You can read more about it in this answer:
How can I create an CABasicAnimation for multiple properties?
I was trying to do a "flip x animation" for a UIButton. I have seen some documentation on how to do this animation but i couldn't find how to do it with repetition. For example, i want to flip the image of the UIButton and change it after a time interval of 5 sec with repetition.
Any help? Thanks.
You can use CAAnimationGroup to group an array of animations. Total animation for whole animationGroup is 6.0, flip animation takes place for 1.0 second. so remaining 5.0 seconds act as delay here.
EDIT: CATransaction.setCompletionBlock can be used but it will only call completion after all animations are completed. So now the flip will occur only for one time and on completion you will receive completion call, and update image view and initiate flip animation again.
Like this
imageView.flip {
// update imageView and call it again.
}
extension UIView {
func flip(completion: #escaping ()->()) {
CATransaction.begin()
CATransaction.setCompletionBlock {
// completion code here
completion()
}
let animationGroup = CAAnimationGroup()
animationGroup.duration = 6.0
animationGroup.repeatCount = 1
let easeInOut = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
let animation = CABasicAnimation(keyPath: "transform")
animation.duration = 1.0
animation.autoreverses = true
animation.fromValue = CATransform3DIdentity
animation.toValue = NSValue(caTransform3D: CATransform3DMakeRotation(CGFloat(M_PI), 0, 1, 0))
animation.timingFunction = easeInOut
animationGroup.animations = [animation]
layer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
layer.add(animationGroup, forKey: "flip")
CATransaction.commit()
}
}
My ViewController has an UIImageView (lets name it img) as atribute, and in the beginning, it shows img in full screen, that is, its size is a rectangle similar to the ViewController bounds.
I'd like to do an animation with img, making it become a little circle (not just a rectangle with circular corners) and then move it to some corner of the ViewController.
How can I achieve this? I prefer not to send any code cos Im pretty sure I did it the wrong way (I tried to turn it into a square and then set a cornerRadius (with radius width/2), then making scale and translating position at once, but its a mess and works wrongly, so lets forget it)
Your solution sounds fine to me, but what might be going wrong, is the fact that animating corner radius is not supported by UIView.animateWithDuration as seen in the View Programming Guide for iOS. Therefor, the results are sometimes not whats expected.
You can try out the following code.
func animate() {
//Some properties that needs to be set on UIImageView
self.imageView.clipsToBounds = true
self.imageView.contentMode = .ScaleAspectFill
//Create animations
let cornerRadiusAnim = changeCornerRadiusAnimation()
let squareAnim = makeSquareAnimation()
let boundsAnim = changeBoundsAnimation()
let positionAnim = changePositionAnimation(CGPointMake(300,480))
//Use group for sequenced execution of animations
let animationGroup = CAAnimationGroup()
animationGroup.animations = [cornerRadiusAnim, squareAnim, boundsAnim, positionAnim]
animationGroup.fillMode = kCAFillModeForwards
animationGroup.removedOnCompletion = false
animationGroup.duration = positionAnim.beginTime + positionAnim.duration
imageView.layer.addAnimation(animationGroup, forKey: nil)
}
func makeSquareAnimation() -> CABasicAnimation {
let center = imageView.center
let animation = CABasicAnimation(keyPath:"bounds")
animation.duration = 0.1;
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
animation.fromValue = NSValue(CGRect: imageView.frame)
animation.toValue = NSValue(CGRect: CGRectMake(center.x,center.y,imageView.frame.width,imageView.frame.width))
animation.fillMode = kCAFillModeForwards
animation.removedOnCompletion = false
return animation
}
func changeBoundsAnimation() -> CABasicAnimation {
let animation = CABasicAnimation(keyPath:"transform.scale")
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
animation.fromValue = imageView.layer.mask?.valueForKeyPath("transform.scale")
animation.toValue = 0.1
animation.duration = 0.5
animation.beginTime = 0.1
animation.fillMode = kCAFillModeForwards
animation.removedOnCompletion = false
return animation
}
func changeCornerRadiusAnimation() -> CABasicAnimation {
let animation = CABasicAnimation(keyPath:"cornerRadius")
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
animation.fromValue = 0
animation.toValue = imageView.frame.size.width * 0.5
animation.duration = 0.1
animation.fillMode = kCAFillModeForwards
animation.removedOnCompletion = false
return animation
}
func changePositionAnimation(newPosition: CGPoint) -> CABasicAnimation {
let animation = CABasicAnimation(keyPath: "position")
animation.fromValue = NSValue(CGPoint: imageView.layer.position)
animation.toValue = NSValue(CGPoint: newPosition)
animation.duration = 0.3
animation.beginTime = 0.6
animation.fillMode = kCAFillModeForwards
animation.removedOnCompletion = false
return animation
}
// try like this, i hope it will work for you.
UIView.animateWithDuration(3, delay: 0.0, options: UIViewAnimationOptions.CurveEaseIn, animations: { () -> Void in
img.layer.masksToBounds = true
img.layer.cornerRadius = img.frame.size.height / 2 // here img must be in square shape (height == width)
}) { (finished : Bool) -> Void in
}
option 1 call below method from code
// UIimageView Circle
class func roundImageView(imageView:UIImageView){
imageView.layer.borderWidth = 1.0
imageView.layer.masksToBounds = false
//imageView.layer.borderColor = UIColor.whiteColor().CGColor
imageView.layer.cornerRadius = imageView.frame.size.width/2
imageView.clipsToBounds = true
}
option 2 make user defined run time attributes from storyboard
view.layer.cornerRadius = 5.0f;
view.layer.masksToBounds = NO;
I'm building a simple UIView subclass that plots some data as a line graph. I'd like to animate the data points as they come in, but as a newcomer to CAShapeLayer, I feel there's got to be a better way than what I'm doing. I've read a few articles and put together some code loosely based on this article.
Here's the code. Essentially it redraws the entire path every time new data are available, but only animates the new section (well, approximately).
let animationLayer = CALayer()
func setupDrawingLayer(){
pathLayer.frame = self.animationLayer.bounds
pathLayer.bounds = self.animationLayer.bounds
pathLayer.geometryFlipped = true
pathLayer.strokeColor = UIColor.blackColor().CGColor
pathLayer.fillColor = nil
pathLayer.lineWidth = 3.0
pathLayer.lineJoin = kCALineJoinBevel
self.animationLayer.addSublayer(pathLayer)
}
func nextPoint(){ // This is called by a timer to simulate a new data point
let path = UIBezierPath()
path.moveToPoint(self.dataPoints[0])
for i in 1...pointIndex{
path.addLineToPoint(self.dataPoints[i])
}
pathLayer.path = path.CGPath
pointIndex += 1
let pathAnimation = CABasicAnimation(keyPath:"strokeEnd")
pathAnimation.duration = 0.5
pathAnimation.fromValue = (Double(pointIndex) - 1.0) / Double(pointIndex)
pathAnimation.toValue = 1.0
self.pathLayer.addAnimation(pathAnimation, forKey:"strokeEnd")
}
Also, as my fromValue weighting is just an approximation, it doesn't lead to a smooth animation. Ideally, I would think the approach in nextPoint() should be to animate only the newest section, then "save" it to the layer… but not sure how to approach that. Any pointers would be very welcome.
UPDATE:
This approach still recreates the path with each call of nextPoint() but at least the animation is now accurate:
func nextPoint(){
let oldPath = pathLayer.path
let path = UIBezierPath()
path.moveToPoint(self.dataPoints[0])
for i in 1...pointIndex{
path.addLineToPoint(self.dataPoints[i])
}
pathLayer.path = path.CGPath
pointIndex += 1
let pathAnimation = CABasicAnimation(keyPath:"path")
pathAnimation.duration = 0.5
pathAnimation.fromValue = oldPath
pathAnimation.toValue = path.CGPath
self.pathLayer.addAnimation(pathAnimation, forKey:"path")
}