Repeating Keyframe animation N times - ios

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)

Related

How to chain animations using key frames

I want the arranged subviews of a stack view to become visible in a cascaded manner, but they're all appearing at the same time:
let i = 5
let animation = UIViewPropertyAnimator(duration: 5, timingParameters: UICubicTimingParameters())
animation.addAnimations {
UIView.animateKeyframes(withDuration: 0, delay: 0, options: .calculationModeCubicPaced) {
for index in 0..<i {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: Double(1/i)) {
let label = self.stackView.arrangedSubviews[index]
label.alpha = 1
}
}
} completion: { (_) in
return
}
}
animation.startAnimation()
My expectation is that setting the options to calculationModePaced or calculationModeCubicPaced animates each key frame in an evenly spaced-out manner even if I don't set the withRelativeStartTime parameter individually (and have them at 0).
I've tried changing the option to calculationModeLinear and manually setting the withRelativeStartTime:
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.5) {
let label = self.stackView.arrangedSubviews[0]
label.alpha = 1
}
UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.5) {
let label = self.stackView.arrangedSubviews[1]
label.alpha = 1
}
but, the labels still show up at the same time.
To make it work as you expected you should:
remove , options: .calculationModeCubicPaced
fix relativeDuration - 1.0 / Double(i)
setup withRelativeStartTime - Double(index) / Double(i)
example:
let totalCount = labels.count
let labelDuration = 1.0 / Double(totalCount)
let animation = UIViewPropertyAnimator(duration: 5, timingParameters: UICubicTimingParameters())
animation.addAnimations {
UIView.animateKeyframes(withDuration: 0, delay: 0, animations: { [weak self] in
for i in 0..<totalCount {
UIView.addKeyframe(withRelativeStartTime: Double(i) / Double(totalCount), relativeDuration: labelDuration) {
self?.labels[i].alpha = 1
}
}
})
}
animation.startAnimation()

Rotate a view 360 degrees infinitely

I am using this code to rotate a view 360 degrees infinitely. But my view is not rotating:
func rotateImageView() {
UIView.animate(withDuration: 3.0, delay: 0, options: [.repeat, .curveLinear], animations: {
self.vinylView.transform = CGAffineTransform(rotationAngle: .pi * 2)
})
}
How to fix it?
Substitute pi * with /, delete repeat, add completion and recall the function like this:
private func rotateImageView() {
UIView.animate(withDuration: 3, delay: 0, options: .curveLinear, animations: {
self.vinylView.transform = self.vinylView.transform.rotated(by: .pi / 2)
}) { (finished) in
if finished {
self.rotateImageView()
}
}
}
Solution using CABasicAnimation
// Rotate vinvlView
vinylView.layer.add(CABasicAnimation.rotation, forKey: nil)
extension CABasicAnimation {
static let rotation : CABasicAnimation = {
let animation = CABasicAnimation(keyPath: "transform.rotation.z")
animation.repeatCount = .infinity // Rotate a view 360 degrees infinitely
animation.fromValue = 0
animation.toValue = CGFloat.pi * 2
animation.duration = 3.0
return animation
}()
}

[CAKeyframeAnimation setFromValue:]: unrecognized selector sent to instance

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?) {
}
}

UIView .animate not working vs .animateKeyframes

I am trying to figure out why UIView.animate does not work as intended.
I know with UIView.animateKeyframes I can chain multiple types of animations together, but in this instance I am doing just one animation transform from the current size to 0.0.
The spinner image is already spinning using CABasicAnimation prior to this transform using:
let spinAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
spinAnimation.toValue = .pi * 2.0 // 360ยบ spin
spinAnimation.duration = 1.4
spinAnimation.repeatCount = Float.greatestFiniteMagnitude // Repeat forever in loops.
spinAnimation.isCumulative = true
spinAnimation.isRemovedOnCompletion = false
self.spinnerImageView.layer.add(spinAnimation, forKey: self.spinnerAnimationKey)
UIView KeyFrame Animation
UIView.animateKeyframes(withDuration: Constants.AnimationDuration.standard, delay: 0.0, options: .calculationModeLinear, animations: {
UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.4, animations: {
self.spinner.transform = CGAffineTransform(scaleX: 0.0, y: 0.0)
})
}) { (_) in
super.endRefreshing()
}
UIView Animation
UIView.animate(withDuration: 0.4, delay: 0.0, options: .curveLinear, animations: {
self.spinner.transform = CGAffineTransform(scaleX: 0.0, y: 0.0)
}) { (_) in
super.endRefreshing()
}
The keyframe animation works as intended where it shows the animating image shrink to 0.0, but the standard animate just causes the image to disappear. Shouldn't they act in the same way or is it because the image already has a CABasicAnimation layer that is causing the animate to not work properly? I would prefer to use .animate since I am only doing a transformation.

UIView animate - Rotate and scale up, then rotate and scale down

I have a subclassed imageview that I'd like to fade in, scale up, and rotate, then continue rotating while scaling back down and fading out.
I am using a UIView animate block with a completion handler to handle the shrinking back down.
The problem is it's not a fluid animation. Before the completion handler runs, the animation stops before running again. I need it to be one nice "swoop" of an animation.
Code below:
let duration: TimeInterval = 3.0
let rotate = CGAffineTransform(rotationAngle: CGFloat(Double.pi))
UIView.animate(withDuration: duration * 3, delay: 0, options: [.curveLinear], animations: {
// initial transform
self.alpha = 0
self.transform = CGAffineTransform(scaleX: 0.1, y: 0.1)
// initial spin for duration of animaton
UIView.animate(withDuration: duration * 3, delay: 0.0, options: [.curveLinear], animations: {
self.transform = rotate
}, completion: nil)
// scaling and fading
UIView.animate(withDuration: duration, delay: 0, options: [.curveLinear], animations: {
UIView.setAnimationRepeatCount(3)
self.transform = self.transform.scaledBy(x: 0.8, y: 0.8)
self.alpha = 1
}) { (true) in
UIView.animate(withDuration: duration, animations: {
UIView.setAnimationRepeatCount(3)
self.transform = self.transform.scaledBy(x: 0.1, y: 0.1)
self.alpha = 0
})
}
}, completion: nil)
How can I get the animation to rotate the entire time while fading in and scaling up before scaling back down and fading out? The entire animation should last 3 seconds, and repeat 3 times. Thanks.
For your modified request I am expanding what you already saw using the block animations. As some have said, key frame animations may be better, regardless, here is the thought.
Create an animation that rotates the entire time by transforming the view.
Create another animation that does the scaling and fading based off the current transform (which is rotating). In this pass, I just created some variable to allow you to customize (and repeat) portions of the animation. I broke some things out to be clear and know I could refactor to write thing even more concise.
Here is the code
import UIKit
class OrangeView: UIView {
override func layoutSubviews() {
super.layoutSubviews()
let duration: TimeInterval = 9.0
self.transform = CGAffineTransform.identity
// initial transform
self.alpha = 1
self.transform = CGAffineTransform(scaleX: 0.1, y: 0.1)
// start rotation
rotate(duration: duration)
// scaling and fading
scaleUpAndDown(desiredRepetitions: 3, initalDuration: duration)
}
func rotate(duration: TimeInterval) {
UIView.animate(withDuration: duration/2.0,
delay: 0.0,
options: [.curveLinear], animations: {
let angle = Double.pi
self.transform = self.transform.rotated(by: CGFloat(angle))
}, completion: {[weak self] finished in
guard let strongSelf = self else {
return
}
if finished &&
strongSelf.transform != CGAffineTransform.identity {
strongSelf.rotate(duration: duration)
} else {
// rotation ending
}
})
}
func scaleUpAndDown(timesRepeated: Int = 0, desiredRepetitions: Int, initalDuration: TimeInterval) {
guard timesRepeated < desiredRepetitions,
desiredRepetitions > 0, initalDuration > 0 else {
self.transform = CGAffineTransform.identity
return
}
let repeatedCount = timesRepeated + 1
let scalingDuration = initalDuration/2.0/Double(desiredRepetitions)
UIView.animate(withDuration: scalingDuration,
delay: 0.0,
animations: {
let desiredOriginalScale: CGFloat = 0.8
let scaleX = abs(CGAffineTransform.identity.a / self.transform.a) * desiredOriginalScale
let scaleY = abs(CGAffineTransform.identity.d / self.transform.d) * desiredOriginalScale
self.transform = self.transform.scaledBy(x: scaleX, y: scaleY)
self.alpha = 1
}) { (true) in
UIView.animate(withDuration:scalingDuration,
delay: 0.0,
animations: {
let desiredOriginalScale: CGFloat = 0.1
let scaleX = abs(CGAffineTransform.identity.a / self.transform.a) * desiredOriginalScale
let scaleY = abs(CGAffineTransform.identity.d / self.transform.d) * desiredOriginalScale
self.transform = self.transform.scaledBy(x: scaleX, y: scaleY)
self.alpha = 0
}) { finshed in
self.scaleUpAndDown(timesRepeated: repeatedCount, desiredRepetitions: desiredRepetitions, initalDuration: initalDuration);
}
}
}
}
Finally here is another animated gif
I see the slight stutter in rotation at the start of the onCompletion.
I created a reduction with your code (shown below in the Blue View) and a variation in the Orange View. This was taken from the simulator and turned into an animated GIF, so speed is slowed down. The Orange View continues to spin as the complete transform just scales down.
This is the code for the layoutSubviews() for the Orange View
override func layoutSubviews() {
super.layoutSubviews()
let duration: TimeInterval = 3.0
let rotate = CGAffineTransform(rotationAngle: CGFloat(Double.pi))
// initial transform
self.alpha = 0
self.transform = CGAffineTransform(scaleX: 0.1, y: 0.1)
// initial spin for duration of animaton
UIView.animate(withDuration: duration,
delay: 0.0,
options: [.curveLinear],
animations: {
self.transform = rotate;
},
completion: nil)
// scaling and fading
UIView.animate(withDuration: duration/2.0, animations: {
self.transform = self.transform.scaledBy(x: 0.8, y: 0.8)
self.alpha = 1
}) { (true) in
UIView.animate(withDuration: duration/2.0, animations: {
self.transform = self.transform.scaledBy(x: 0.1, y: 0.1)
self.alpha = 0
})
}
}
Try using CGAffineTransformConcat()
CGAffineTransform scale = CGAffineTransformMakeScale(0.8, 0.8);
self.transform = CGAffineTransformConcat(CGAffineTransformRotate(self.transform, M_PI / 2), scale);

Resources