CABasicAnimation chaining not working - swift

I am having difficulties chaining multiple CABasicAnimations. I just want to make a button scale to 1.5 times it's size, then to 0.5 and fade away (while scaling down).
The problem is that only the scaling up works then the button goes back to the original state.
I would have used CGAffineTransformMakeScale but it creates a bug in iOS 7. On iOS 8 & 9 it works fine
I tried using a CAAnimationGroup but same result.
Here's the code:
let scaleUp = CABasicAnimation(keyPath: "transform.scale")
scaleUp.fromValue = 1
scaleUp.toValue = 1.5
scaleUp.duration = 0.2
scaleUp.beginTime = 0
scaleUp.removedOnCompletion = true
self.myButton.layer.addAnimation(scaleUp, forKey: "up")
let scaleDown = CABasicAnimation(keyPath: "transform.scale")
scaleDown.fromValue = 1
scaleDown.toValue = 0.5
scaleDown.duration = 0.2
scaleDown.beginTime = scaleUp.beginTime + scaleDown.duration
scaleDown.removedOnCompletion = true
self.myButton.layer.addAnimation(scaleDown, forKey: "down")
let fade = CABasicAnimation(keyPath: "opacity")
fade.fromValue = 1
fade.toValue = 0
fade.duration = 0.2
fade.beginTime = scaleUp.beginTime + scaleDown.duration
fade.removedOnCompletion = false
self.myButton.layer.addAnimation(fade, forKey: "fade")

that works for me quite well, feel free to play with the duration times, or other animation properties.
I tried to use your instructions for the animation, but it does not mean I have set all properties correctly for your expectations.
Swift (2.x)
func addGrowShrinkAndFadeOutAnimationToView(viewToAnimate: UIView) {
let easeInOutTiming = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
let viewScaleXAnimation = CAKeyframeAnimation(keyPath: "transform.scale.x")
viewScaleXAnimation.duration = 0.4
viewScaleXAnimation.values = [1.0 as Float, 1.5 as Float, 0.5 as Float]
viewScaleXAnimation.keyTimes = [0.0 as Float, 0.5 as Float, 1.0 as Float]
viewScaleXAnimation.timingFunctions = [easeInOutTiming, easeInOutTiming]
viewToAnimate.layer.addAnimation(viewScaleXAnimation, forKey:"Grow, Shrink and Fade out_ScaleX")
let viewScaleYAnimation = CAKeyframeAnimation(keyPath: "transform.scale.y")
viewScaleYAnimation.duration = 0.4
viewScaleYAnimation.values = [1.0 as Float, 1.5 as Float, 0.5 as Float]
viewScaleYAnimation.keyTimes = [0.0 as Float, 0.5 as Float, 1.0 as Float]
viewScaleYAnimation.timingFunctions = [easeInOutTiming, easeInOutTiming]
viewToAnimate.layer.addAnimation(viewScaleYAnimation, forKey:"Grow, Shrink and Fade out_ScaleY")
let viewOpacityAnimation = CAKeyframeAnimation(keyPath: "opacity")
viewOpacityAnimation.duration = 0.4
viewOpacityAnimation.values = [1.0 as Float, 1.0 as Float, 0.0 as Float]
viewOpacityAnimation.keyTimes = [0.0 as Float, 0.5 as Float, 1.0 as Float]
viewOpacityAnimation.timingFunctions = [easeInOutTiming, easeInOutTiming]
viewToAnimate.layer.addAnimation(viewOpacityAnimation, forKey:"Grow, Shrink and Fade out_Opacity")
}

Is there a specific reason you want to use CA animations? I find manipulating constraints easier. I have added a button in storyboard and created outlets for the height and width constraints of the button.
#IBOutlet weak var okButton: UIButton!
#IBOutlet weak var widthConstraint: NSLayoutConstraint!
#IBOutlet weak var heightConstraint: NSLayoutConstraint!
var buttonWidth : CGFloat?
var buttonHeight : CGFloat?
override func viewDidLoad() {
super.viewDidLoad()
self.buttonHeight = self.heightConstraint.constant
self.buttonWidth = self.widthConstraint.constant
}
#IBAction func okButtonTapped(button: UIButton) {
let animationDuration = 2.0
self.heightConstraint.constant = self.buttonWidth! * 2.0
self.widthConstraint.constant = self.buttonHeight! * 2.0
UIView.animateWithDuration(animationDuration, animations: {
button.layoutIfNeeded()
}) { (finished) in
self.heightConstraint.constant = self.buttonWidth! * 0.5
self.widthConstraint.constant = self.buttonHeight! * 0.5
UIView.animateWithDuration(animationDuration, animations: {
button.alpha = 0.0
button.layoutIfNeeded()
}, completion: { (finished) in
button.hidden = true
})
}
}

Related

CAAnimationGroup timing

This is strange to me
For the following code (where myView is a coloured UIView on the storyboard) the timing seems off.
class ViewController: UIViewController {
#IBOutlet weak var myView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let round = CABasicAnimation(keyPath: "cornerRadius")
round.fromValue = 0.0
round.toValue = 50.0
round.duration = 2.0
round.beginTime = 0.0
round.fillMode = CAMediaTimingFillMode.backwards
round.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn)
myView.layer.cornerRadius = 50.0
let scaleDown = CABasicAnimation(keyPath: "transform.scale")
scaleDown.fromValue = 1.0
scaleDown.toValue = 0.25
scaleDown.beginTime = 2.0
scaleDown.duration = 2.0
let rotate = CABasicAnimation(keyPath: "transform.rotation")
rotate.fromValue = .pi/10.0
rotate.toValue = 0.0
rotate.beginTime = 4.0
rotate.duration = 2.0
let hideAnimation = CABasicAnimation(keyPath: "hidden")
hideAnimation.fromValue = NSNumber(value: 1)
hideAnimation.toValue = NSNumber(value: 0)
hideAnimation.beginTime = 4.0
hideAnimation.duration = 2.0
let imageGroupAnimations = CAAnimationGroup()
imageGroupAnimations.animations = [round, scaleDown, rotate, hideAnimation]
imageGroupAnimations.duration = 6.0
imageGroupAnimations.repeatCount = 1
imageGroupAnimations.beginTime = 0.0
myView.layer.add(imageGroupAnimations, forKey: nil)
}
}
The duration of the animation group is 6.0, so I would expect the shape to be hidden (since I expect it to be hidden from 4 to 4 + 2 (6.0) seconds.
However, the shape appears and finishes off the animation (finish rotating). I've tried using CACurrentMediaTime() in front of every beginTime - but this also doesn't have the expected result.
Expected result:
0-2 seconds animate corner radius change
2-4 seconds animate scale transformation
4-6 seconds animate rotation (hidden from the user)
How can I make the shape disappear from 4 - 6 seconds?
If you want to show fade-out effect on view, use
let hideAnimation = CABasicAnimation(keyPath: "opacity")
Instead of
let hideAnimation = CABasicAnimation(keyPath: "hidden") //bad!
If you don't want animation to reset your values after finish, simply add
imageGroupAnimations.fillMode = .forwards
imageGroupAnimations.isRemovedOnCompletion = false
to your CAAnimationGroup
The duration is not how long something stays true — it is how long it takes to perform a change.
So if you want the view to vanish suddenly, obviously you need to change hideAnimation.duration = 2.0 so that the duration is very short, say, 0.01.
This is a hideAnimation where the view turns invisible suddenly at the 4th second of the animation, and stays invisible until the end of the animation:
let hideAnimation = CABasicAnimation(keyPath: "hidden")
hideAnimation.fromValue = false
hideAnimation.toValue = true
hideAnimation.beginTime = 4.0
hideAnimation.duration = 0.01
hideAnimation.fillMode = .forwards
But I did not make any other changes because I still do not understand the complete effect that you want...

CABasicAnimation to emulate a 'pulse' effect animation on a non-circle shape

I am using CBasicAnimation to create a pulsating effect on a button.
The effect pulses out the shape of a UIView, with border only.
While the animation works properly, I am not getting the desired effect using CABasicAnimation(keyPath: "transform.scale").
I am using an animation group with 3 animations: borderWidth, transform.scale and opacity.
class Pulsing: CALayer {
var animationGroup = CAAnimationGroup()
var initialPulseScale:Float = 1
var nextPulseAfter:TimeInterval = 0
var animationDuration:TimeInterval = 1.5
var numberOfPulses:Float = Float.infinity
override init(layer: Any) {
super.init(layer: layer)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
init (numberOfPulses:Float = Float.infinity, position:CGPoint, pulseFromView:UIView, rounded: CGFloat) {
super.init()
self.borderColor = UIColor.black.cgColor
self.contentsScale = UIScreen.main.scale
self.opacity = 1
self.numberOfPulses = numberOfPulses
self.position = position
self.bounds = CGRect(x: 0, y: 0, width: pulseFromView.frame.width, height: pulseFromView.frame.height)
self.cornerRadius = rounded
DispatchQueue.global(qos: DispatchQoS.QoSClass.default).async {
self.setupAnimationGroup(view: pulseFromView)
DispatchQueue.main.async {
self.add(self.animationGroup, forKey: "pulse")
}
}
}
func borderWidthAnimation() -> CABasicAnimation {
let widthAnimation = CABasicAnimation(keyPath: "borderWidth")
widthAnimation.fromValue = 2
widthAnimation.toValue = 0.5
widthAnimation.duration = animationDuration
return widthAnimation
}
func createScaleAnimation (view:UIView) -> CABasicAnimation {
let scale = CABasicAnimation(keyPath: "transform.scale")
DispatchQueue.main.async {
scale.fromValue = view.layer.value(forKeyPath: "transform.scale")
}
scale.toValue = NSNumber(value: 1.1)
scale.duration = 1.0
scale.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
return scale
}
func createOpacityAnimation() -> CABasicAnimation {
let opacityAnimation = CABasicAnimation(keyPath: "opacity")
opacityAnimation.duration = animationDuration
opacityAnimation.fromValue = 1
opacityAnimation.toValue = 0
opacityAnimation.fillMode = .removed
return opacityAnimation
}
func setupAnimationGroup(view:UIView) {
self.animationGroup = CAAnimationGroup()
self.animationGroup.duration = animationDuration + nextPulseAfter
self.animationGroup.repeatCount = numberOfPulses
self.animationGroup.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.default)
self.animationGroup.animations = [createScaleAnimation(view: view), borderWidthAnimation(), createOpacityAnimation()]
}
}
class ViewController: UIViewController {
#IBOutlet weak var pulsingView: UIView!
let roundd:CGFloat = 20
override func viewDidLoad() {
super.viewDidLoad()
pulsingView.layer.cornerRadius = roundd
let pulse = Pulsing(
numberOfPulses: .greatestFiniteMagnitude,
position: CGPoint(x: pulsingView.frame.width/2,
y: pulsingView.frame.height/2)
, pulseFromView: pulsingView, rounded: roundd)
pulse.zPosition = -10
self.pulsingView.layer.insertSublayer(pulse, at: 0)
}
}
My problem is transform.scale is maintaining the aspect ratio of the UIView it's pulsating from during the animation.
How can I make the pulse grow so there's uniform spacing on both the height and the width? See screenshot.
Scaling the width and height by the same factor is going to result in unequal spacing around the edges. You need to increase the layer's width and height by the same value. This is an addition operation, not multiplication. Now, for this pulsating effect you need to animate the layer's bounds.
If you want the spacing between the edges to be dynamic, then pick a scale factor and apply it to a single dimension. Whether you choose the width or the the height doesn't matter so long as it's only applied to one. Let's say you choose the width to grow by a factor of 1.1. Compute your target width, then compute the delta.
let scaleFactor: CGFloat = 1.1
let targetWidth = view.bounds.size.width * scaleFactor
let delta = targetWidth - view.bounds.size.width
Once you have your delta, apply it to the layer's bounds in the x and the y dimension. Take advantage of the insetBy(dx:) method to compute the resulting rectangle.
let targetBounds = self.bounds.insetBy(dx: -delta / 2, dy: -delta / 2)
For clarity's sake, I've renamed your createScaleAnimation(view:) method to createExpansionAnimation(view:). Tying it all together we have:
func createExpansionAnimation(view: UIView) -> CABasicAnimation {
let anim = CABasicAnimation(keyPath: "bounds")
DispatchQueue.main.async {
let scaleFactor: CGFloat = 1.1
let targetWidth = view.bounds.size.width * scaleFactor
let delta = targetWidth - view.bounds.size.width
let targetBounds = self.bounds.insetBy(dx: -delta / 2, dy: -delta / 2)
anim.duration = 1.0
anim.fromValue = NSValue(cgRect: self.bounds)
anim.toValue = NSValue(cgRect: targetBounds)
}
return anim
}

What is the proper way to end a CAEmitterLayer in Swift?

I've mostly seen examples of continuous emitters in Swift, and I've found one example in Obj-C by setting the birthRates of the emitter cells to 0.0, but it doesn't seem to work, so I must be doing something wrong. In my example, I can see the message that the birth rate was set to 0 sixteen times, but the particles continue to flow endlessly.
#IBAction func particleBtnAction(_ sender: Any) {
let emitter = CAEmitterLayer()
emitter.emitterPosition = CGPoint(x: self.view.frame.size.width / 2, y: -10)
emitter.emitterShape = kCAEmitterLayerLine
emitter.emitterSize = CGSize(width: self.view.frame.size.width, height: 2.0)
emitter.emitterCells = generateEmitterCells()
self.view.layer.addSublayer(emitter)
// perform selector after 1.5 seconds when particles start
perform(#selector(endParticles), with: emitter, afterDelay: 1.5)
}
private func generateEmitterCells() -> [CAEmitterCell] {
var cells:[CAEmitterCell] = [CAEmitterCell]()
for index in 0..<16 {
let cell = CAEmitterCell()
cell.birthRate = 4.0
cell.lifetime = 1.0
cell.lifetimeRange = 0
cell.velocity = 0.7
cell.velocityRange = 0
cell.emissionLongitude = CGFloat(Double.pi)
cell.emissionRange = 0.5
cell.spin = 3.5
cell.spinRange = 0
cell.scaleRange = 0.25
cell.scale = 0.1
cells.append(cell)
}
return cells
}
#objc func endParticles(emitterLayer:CAEmitterLayer) {
for emitterCell in emitterLayer.emitterCells! {
emitterCell.birthRate = 0.0
print("birth rate set to 0")
}
}
Setting the CAEmitterLayer's lifetime to zero stops any new emitterCells being emitted:
#objc func endParticles(emitterLayer:CAEmitterLayer) {
emitterLayer.lifetime = 0.0
}
You can use key paths to assign a name to each cell and loop through them, changing each cell's property when you want to change them:
private func generateEmitterCells() -> [CAEmitterCell] {
var cells:[CAEmitterCell] = [CAEmitterCell]()
for index in 0..<16 {
let cell = CAEmitterCell()
cell.birthRate = 4.0
cell.lifetime = 1.0
cell.lifetimeRange = 0
cell.velocity = 0.7
cell.velocityRange = 0
cell.emissionLongitude = CGFloat(Double.pi)
cell.emissionRange = 0.5
cell.spin = 3.5
cell.spinRange = 0
cell.scaleRange = 0.25
cell.scale = 0.1
cell.name = "cell\(index)" // cell name
cells.append(cell)
}
return cells
}
#objc func endParticles(emitterLayer:CAEmitterLayer) {
for i in 0..<(emitterLayer.emitterCells?.count ?? 0){
emitterLayer.setValue(0.0, forKeyPath: "emitterCells.cell\(i).birthRate")
print("birth rate set to 0")
}
}
You might try setting the isHidden property when you want to endParticles
emitter.isHidden = true
But note that all the cells instantly vanish, no matter when they were emitted, or their lifetime.
Another possibility would be to set all the scale related properties to 0, and then the lifetime and birthrate would not matter, as newly emitted cells would not be visible.
cell.scaleSpeed = 0
cell.scaleRange = 0
cell.scale = 0
What worked for me is this:
let emmitter = CAEmitterLayer()
let cell = makeCell()
emmitter.emitterCells = [cell]
view.layer.addSublayer(emmitter)
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
emmitter.removeFromSuperlayer()
}

Swift- How to find width/height of a Presentation Layer that is being scaled and rotated?

So, I have a CALayer that is being rotated and scaled using CABasicAnimation. Since the layer is being rotated, frame returns the frame containing the highest to lowest x and y values(so this will be larger than the actual presented layer). And, while bounds would work if scaling does not occur, the bounds of the layer do not. Here is my code for my custom UIView subclass:
func rotateCircleView(duration: CFTimeInterval = 1.0) {
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotateAnimation.fromValue = 0.0
rotateAnimation.toValue = CGFloat(M_PI * 2.0)
rotateAnimation.duration = duration
rotateAnimation.repeatCount = Float.infinity
self.layer.addAnimation(rotateAnimation, forKey: rotateKey)
}
func shrinkCircleView(duration: CFTimeInterval = 1.0, to: Double) {
let from = self.getCurrentAnimationScale()
let shrinkAnimation = CABasicAnimation(keyPath: "transform.scale")
shrinkAnimation.additive = true
shrinkAnimation.fromValue = from - to
shrinkAnimation.toValue = 0
shrinkAnimation.duration = duration
shrinkAnimation.removedOnCompletion = true
let scaleTransform = CATransform3DMakeScale(CGFloat(to), CGFloat(to), 1.0)
layer.transform = scaleTransform
layer.addAnimation(shrinkAnimation, forKey: "shrinkAnimation")
}
func getCurrentAnimationScale() -> Double{
if let temp = self.layer.presentationLayer(){
// return scale of presentationLayer
} else{
// return scale of self.layer
}
}

SKEmitterNode iOS 8 vs iOS 9 How to get the same result?

I have a game similar to fruit ninja using Swift -> SpriteKit. Everything is working fine on iOS 8 but on iOS 9 SKEmitterNode is having a bit strange behavior. This is what I get for my blade effect on both:
func emitterNodeWithColor(color:UIColor)->SKEmitterNode {
let emitterNode:SKEmitterNode = SKEmitterNode()
emitterNode.particleTexture = SKTexture(imageNamed: "spark.png")
emitterNode.particleBirthRate = 3000
emitterNode.particleLifetime = 0.2
emitterNode.particleLifetimeRange = 0
emitterNode.particlePositionRange = CGVectorMake(0.0, 0.0)
emitterNode.particleSpeed = 0.0
emitterNode.particleSpeedRange = 0.0
emitterNode.particleAlpha = 0.8
emitterNode.particleAlphaRange = 0.2
emitterNode.particleAlphaSpeed = -0.45
emitterNode.particleScale = 0.5
emitterNode.particleScaleRange = 0.001
emitterNode.particleScaleSpeed = -1
emitterNode.particleRotation = 0
emitterNode.particleRotationRange = 0
emitterNode.particleRotationSpeed = 0
emitterNode.particleColorBlendFactor = 1
emitterNode.particleColorBlendFactorRange = 0
emitterNode.particleColorBlendFactorSpeed = 0
emitterNode.particleColor = color
emitterNode.particleBlendMode = SKBlendMode.Add
return emitterNode
}
let emitter:SKEmitterNode = emitterNodeWithColor(color)
emitter.targetNode = target
emitter.zPosition = 0
tip.addChild(emitter)
This is the method I am using with all the options. It is the same for both but the result is different. Any ideas how can I make the effect in iOS 9 to be the same as iOS 8 ?
I'm facing the exact same issue in my project.
The emitter's performance is low in iOS9 (Metal version not finished?), so Apple shut off the interpolation of the drawing to get back the performance a little (The drawing rate is limited to 60 fps, anything between two frames is not rendered).
My solution is to implement the tail myself, which is simple:
class TailNode: SKSpriteNode {
var tailTexture: SKTexture!
var tailSize: CGSize! = CGSizeMake(30, 30)
var tailColor: SKColor!
var tailBlendMode: SKBlendMode!
var initialAlpha: CGFloat = 0.6
var initialScale: CGFloat = 0
var finalScale: CGFloat = 1
var particleLife: NSTimeInterval = 0.1
var running: Bool = false
var particleAction: SKAction!
var lastParticle: SKSpriteNode?
var battleScene: BattleScene {
return self.scene as! BattleScene
}
convenience init(tailTexture: SKTexture, tailSize: CGSize, tailColor: SKColor, tailBlendMode: SKBlendMode, initialAlpha: CGFloat, initialScale: CGFloat, finalScale: CGFloat, particleLife: NSTimeInterval) {
self.init(texture: nil, color: SKColor.whiteColor(), size: CGSize(width: 0, height: 0))
self.tailTexture = tailTexture
self.tailSize = tailSize
self.tailColor = tailColor
self.tailBlendMode = tailBlendMode
self.initialAlpha = initialAlpha
self.initialScale = initialScale
self.finalScale = finalScale
self.particleLife = particleLife
let fadeAction = SKAction.fadeAlphaTo(0, duration: particleLife)
let scaleAction = SKAction.scaleTo(finalScale, duration: particleLife)
let removeAction = SKAction.removeFromParent()
self.particleAction = SKAction.sequence([SKAction.group([fadeAction, scaleAction]), removeAction])
}
func updateWithTimeSinceLastUpdate(interval: NSTimeInterval) {
if running {
let particlePosition = battleScene.convertPoint(battleScene.convertPoint(self.position, fromNode: self.parent!),
toNode:battleScene.worldLayers[.UnderCharacter]!)
if lastParticle == nil || lastParticle!.parent == nil {
lastParticle = nil
} else {
let lastPosition = lastParticle!.position
let x = lastPosition.x + (particlePosition.x - lastPosition.x)*0.5
let y = lastPosition.y + (particlePosition.y - lastPosition.y)*0.5
newParticleAtPosition(CGPointMake(x, y), withDelay: interval*0.5)
}
lastParticle = newParticleAtPosition(particlePosition, withDelay: interval)
}
}
func newParticleAtPosition(position: CGPoint, withDelay delay: NSTimeInterval) -> SKSpriteNode {
let myParticle = SKSpriteNode(texture: tailTexture, color: tailColor, size: tailSize)
myParticle.colorBlendFactor = 1
myParticle.blendMode = tailBlendMode
myParticle.alpha = initialAlpha
myParticle.setScale(initialScale)
myParticle.position = position
battleScene.addNode(myParticle, atWorldLayer: .UnderCharacter)
myParticle.runAction(SKAction.sequence([SKAction.waitForDuration(delay), particleAction]))
return myParticle
}
}