[CAKeyframeAnimation setFromValue:]: unrecognized selector sent to instance - ios

I'm trying to animate a button with keyframeanimate with a button that I want to modify it's tintColor and buttons width constraint that I want to scale it 2x and vise versa, here is my code
func InitialAnimationToTutorialButton() {
UIView.animateKeyframes(withDuration: 1.5, delay: 0, options: [.repeat], animations: {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.1, animations: {
self.tutorialButton.tintColor = UIColor(rgb: 0xDB4284)
self.tutorialButtonWidth.constant = 60
self.view.layoutIfNeeded()
})
UIView.addKeyframe(withRelativeStartTime: 0.1, relativeDuration: 0.1, animations: {
self.tutorialButton.tintColor = UIColor(rgb: 0x554E6E)
self.tutorialButtonWidth.constant = 30
self.view.layoutIfNeeded()
})
UIView.addKeyframe(withRelativeStartTime: 0.3, relativeDuration: 0.1, animations: {
self.tutorialButton.tintColor = UIColor(rgb: 0xDB4284)
self.tutorialButtonWidth.constant = 60
self.view.layoutIfNeeded()
})
UIView.addKeyframe(withRelativeStartTime: 0.4, relativeDuration: 0.1, animations: {
self.tutorialButton.tintColor = UIColor(rgb: 0x554E6E)
self.tutorialButtonWidth.constant = 30
self.view.layoutIfNeeded()
})
}) { (finishFlag) in
}
}
but when I run it , I've get an exception and like folowing:
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[CAKeyframeAnimation setFromValue:]: unrecognized selector sent to instance 0x2820e06e0'

I realized how to solve this error. self.view.layoutIfNeeded() should be out of blocks so I did it and fix the error, So here the correct code :
func InitialAnimationToTutorialButton() {
UIView.animateKeyframes(withDuration: 1.5, delay: 0, options: [.repeat], animations: {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.1, animations: {
self.tutorialButton.tintColor = UIColor(rgb: 0xDB4284)
self.tutorialButtonWidth.constant = 60
})
UIView.addKeyframe(withRelativeStartTime: 0.1, relativeDuration: 0.1, animations: {
self.tutorialButton.tintColor = UIColor(rgb: 0x554E6E)
self.tutorialButtonWidth.constant = 30
})
UIView.addKeyframe(withRelativeStartTime: 0.3, relativeDuration: 0.1, animations: {
self.tutorialButton.tintColor = UIColor(rgb: 0xDB4284)
self.tutorialButtonWidth.constant = 60
})
UIView.addKeyframe(withRelativeStartTime: 0.4, relativeDuration: 0.1, animations: {
self.tutorialButton.tintColor = UIColor(rgb: 0x554E6E)
self.tutorialButtonWidth.constant = 30
})
self.view.layoutIfNeeded()
}) { (finishFlag) in
}
}

A bug report has been filed on Open Radar for this issue https://openradar.appspot.com/32939029.
The reporter offers a few workarounds:
Not using a spring for the timing parameters
Putting the layoutIfNeeded call in the keyframe animation as well
Add an extension on CAKeyframeAnimation where you define a fromValue and ignore it being set
The 3rd solution is unlikely to affect your animation. You can add the CAKeyframeAnimation extension in Swift...
extension CAKeyframeAnimation {
#objc func setFromValue(_ value: Any?) {
}
}

Related

Repeating Keyframe animation N times

I currently have key frame animation code like so:
UIView.animateKeyframes(withDuration: 1, delay: 0, options: [.repeat, .allowUserInteraction], animations: {
UIView.setAnimationRepeatCount(3)
UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.4, animations: {
self.containerView.backgroundColor = Theme.blue10
})
UIView.addKeyframe(withRelativeStartTime: 0.4, relativeDuration: 0.2, animations: {
self.containerView.backgroundColor = Theme.blue10
})
UIView.addKeyframe(withRelativeStartTime: 0.6, relativeDuration: 0.4, animations: {
self.containerView.backgroundColor = backgroundColor
})
}, completion: nil)
However setAnimationRepeatCount has been deprecated in iOS13. How would I go about repeating a key frame animation without setAnimationRepeatCount?
I looked into UIViewPropertyAnimator but there doesn't seem to be an option to repeat these animations n times.
Use CABasicAnimation extension like that
extension CABasicAnimation {
static let rotation : CABasicAnimation = {
let animation = CABasicAnimation(keyPath: "transform.rotation.z")
animation.repeatCount = 3 // change number of count here
animation.fromValue = 0
animation.toValue = CGFloat.pi * 2
animation.duration = 3.0 // change animation duration for single turn
return animation
}()
}
call it wherever you need it:
yuorView.layer.add(CABasicAnimation.rotation, forKey: nil)

Understanding animateKeyframes relative start times/delays

I'm either being stupid, or misunderstanding how keyframe animations work on iOS (or both!). The two animation blocks below produce different results but I would expect them to be the same:
let duration: TimeInterval = 2
UIView.animateKeyframes(withDuration: duration, delay: 0, animations: {
UIView.addKeyframe(withRelativeStartTime: 0.9, relativeDuration: 0.1, animations: {
self.someView.transform = CGAffineTransform(translationX: 0, y: 150)
})
})
UIView.animateKeyframes(withDuration: duration * 0.1, delay: duration * 0.9, animations: {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 1, animations: {
self.someView.transform = CGAffineTransform(translationX: 0, y: 150)
})
})
Can anyone help me understand why these are different when executed? The first seems to do what I expect, but the second one seems to execute the animation earlier than expected.
You're seeing the effects of the default timing curve on your keyframe animations. The first animation is the last 0.2 seconds of a 2 second animation, and the second animation is the entirety of a 0.2 second animation. The default ease-in-ease-out means that the first animation will be done entirely in the ease-out portion of the curve.
To force a linear curve on both animations you can wrap them inside another animation and set a couple of options:
UIView.animate(withDuration: duration, delay: 0, options: [.curveLinear], animations: {
UIView.animateKeyframes(withDuration: duration, delay: 0, options: [.overrideInheritedDuration, .calculationModeLinear], animations: {
UIView.addKeyframe(withRelativeStartTime: 0.9, relativeDuration: 0.1, animations: {
view1.transform = CGAffineTransform(translationX: 0, y: 150)
})
})
UIView.animateKeyframes(withDuration: duration * 0.1, delay: duration * 0.9, options: [.overrideInheritedDuration, .calculationModeLinear], animations: {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 1, animations: {
view2.transform = CGAffineTransform(translationX: 0, y: 150)
})
})
}, completion: nil)
Which I think you'll agree is horrible-looking code, but I'm assuming this is an intellectual exercise :)

Chaining Animations in Swift

Recently, I have made some code which draws a face. I wanted to animate the face to shake back and forth. Currently I have this code. It rotates once to the right, then more to the left, and then to the original position. However, what if I wanted the head to infinitely shake back and forth(rotate bath and forth). Is it possible to make some sort of recursive function to do this?
#IBAction func shakeHead(_ sender: UITapGestureRecognizer) {
UIView.animate(
withDuration: 0.5,
animations: {
self.faceView.transform = self.faceView.transform.rotated(by: self.shakeAngle)
},
completion:{ finished in
if(finished){
UIView.animate(
withDuration: 0.5,
animations: {
self.faceView.transform = self.faceView.transform.rotated(by: -(self.shakeAngle)*2)
},
completion:{ finished in
if(finished){
UIView.animate(
withDuration: 0.5,
animations: {
self.faceView.transform = self.faceView.transform.rotated(by: self.shakeAngle)
},
completion: nil
)
}
}
)
}
}
)
}
You could call shakeHead from the final completion block.
#IBAction func shakeHead(_ sender: UITapGestureRecognizer) {
UIView.animate(
withDuration: 0.5,
animations: {
self.faceView.transform = self.faceView.transform.rotated(by: self.shakeAngle)
},
completion:{ finished in
if(finished){
UIView.animate(
withDuration: 0.5,
animations: {
self.faceView.transform = self.faceView.transform.rotated(by: -(self.shakeAngle)*2)
},
completion:{ finished in
if(finished){
UIView.animate(
withDuration: 0.5,
animations: {
self.faceView.transform = self.faceView.transform.rotated(by: self.shakeAngle)
},
completion: { finished in
shakeHead(sender)
}
)
}
}
)
}
}
)
}
Even though this is technically a recursive call, it's not a problem due to the asynchronous nature of the code.
Use animateKeyframes for a better chain animation experience
#IBAction func shakeHead(_ sender: UITapGestureRecognizer) {
UIView.animateKeyframes(withDuration: animationDuration, delay: 0, options: [], animations: {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.5) {
self.faceView.transform = self.faceView.transform.rotated(by: self.shakeAngle)
}
UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.5) {
self.faceView.transform = self.faceView.transform.rotated(by: -(self.shakeAngle)*2)
}
UIView.addKeyframe(withRelativeStartTime: 1.0, relativeDuration: 0.5) {
self.faceView.transform = self.faceView.transform.rotated(by: self.shakeAngle)
}
}) { (isFinished) in
shakeHead(sender)
}
}

Adding easing to animateKeyframes

I'm animating the scale of a button using animateKeyframes in Swift 3 using the following code:
UIView.animateKeyframes(withDuration: 4, delay: 1, options: [.repeat, .allowUserInteraction, .calculationModeCubic], animations: {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.1, animations: {
self.myButton.transform = CGAffineTransform(scaleX: 1,y: 1)
})
UIView.addKeyframe(withRelativeStartTime: 0.1, relativeDuration: 0.1, animations: {
self.myButton.transform = CGAffineTransform(scaleX: 0.9,y: 0.9)
})
}, completion: nil)
What I need to do is add easing to each keyframe. I looked up something that uses the following code that can then be passed to animateKeyframes
let animationOptions: UIViewAnimationOptions = .curveEaseIn
let keyframeAnimationOptions: UIViewKeyframeAnimationOptions = UIViewKeyframeAnimationOptions(rawValue: animationOptions.rawValue)
However, it doesn't quite say exactly how to pass the keyframeAnimationOptions variable, and I can't seem to figure it out myself! Can anybody help?
You can use below code to call the keyframeAnimationOptions variable.
let keyframeAnimationOption=UIViewKeyframeAnimationOptions.CalculationModeCubic
UIView.animateKeyframesWithDuration(duration, delay: delay, options: keyframeAnimationOption, animations:
{
/////Your code //////
}, completion: nil)

Swift: Load .Xib with animation

Currently I am loading a new .Xib of class CreateAnAccount: UIView form a button pressed on another view.
At the moment it immediately switches to the Xib which is great, but is there a way of animating this? below is the code in the button.
#IBAction func createAnAccount(sender: AnyObject) {
let createAnAccountView = NSBundle.mainBundle().loadNibNamed("CreateAnAccount", owner: self, options: nil)[0] as! CreateAnAccount
createAnAccountView.frame = CGRectMake(0, 0, UIScreen.mainScreen().applicationFrame.size.width, UIScreen.mainScreen().applicationFrame.size.height + 20)
createAnAccountView.loginHandler = loginHandler
self.addSubview(createAnAccountView)
println("Create An Account Pressed")
}
Swift 4. Just place your animation code into layoutSubviews func. Works perfect for me.
override func layoutSubviews() {
super.layoutSubviews()
animate()
}
func animate() {
self.transform = CGAffineTransform(scaleX: 0.3, y: 2)
UIView.animate(withDuration: 1, delay: 0, usingSpringWithDamping: 0.3, initialSpringVelocity: 0, options: [.allowUserInteraction, .curveEaseOut], animations: {
self.transform = .identity
})
self.alpha = 1
}
Swift 5. #coldembrace answer
func animate() {
self.view.transform = CGAffineTransform(scaleX: 0.3, y: 2)
UIView.animate(withDuration: 1, delay: 0, usingSpringWithDamping: 0.3, initialSpringVelocity: 0, options: [.allowUserInteraction, .curveEaseOut], animations: {
self.view.transform = .identity
})
self.view.alpha = 1
}
You can try like this
UIView.transitionWithView(self.view, duration: 0.5, options:UIViewAnimationOptions.CurveEaseInOut,animations: {self.view.addSubview(createAnAccountView)}, completion: nil)
Moreover you change the UIViewAnimationOptions to have a desired effect.

Resources