UIViewPropertyAnimator reverse animation - ios

I have a simple pulse animation which runs perfectly
UIView.animate(withDuration: 1, animations: {
myAnimatableView.transform = CGAffineTransform(scaleX: 1.2, y: 1.2)
}) { _ in
UIView.animate(withDuration: 1, animations: {
myAnimatableView.transform = .identity
})
}
I want to make the same animation, but now with UIViewPropertyAnimator. Is it possible to replicate this pulse effect elegantly with the new API?
I'm doing it this way
let animator = UIViewPropertyAnimator(duration: 1, curve: .linear) {
animatableView.transform = CGAffineTransform(scaleX: 1.2, y: 1.2)
}
animator.addCompletion { _ in
animator.isReversed = true
animator.startAnimation()
}
animator.startAnimation()
but my animatableView is just stuck scaled to 1.2.

It seems that the property animator removes animations after completion (which would explain that starting it again would have no effect) - see SO question. Based on that, You might try hacking it through pausesOnCompletion, but I would expect that that would not be as easy as you might think. Simply because the documentation says:
When the value of this property is true, the animator remains in the active state when the animation finishes, and it does not execute its completion handler.
That means that completion handler won't get called, if the pauseOnCompletion is set to true. According to the same documentation, you can determine when the animator ended (and thus when you should restart the animation) by observing isRunning property of the animator. However, when I tried to implement that, I found out that KVO on isRunning does not get called during the lifecycle of the animator. Thus my recommendation in the end is to simply use two animators, because it seems much easier to implement:
let animator = UIViewPropertyAnimator(duration: 1, curve: .linear) {
animatableView.transform = CGAffineTransform(scaleX: 1.2, y: 1.2)
}
animator.addCompletion { _ in
let secondAnimator = UIViewPropertyAnimator(duration: 1, curve: .linear) {
animatableView.transform = CGAffineTransform.identity
}
secondAnimator.startAnimation()
}
animator.startAnimation()

I think your code is more appropiated translated like this
Try with this code
let animator = UIViewPropertyAnimator(duration: 1, curve: .linear) {
self.animatableView.transform = CGAffineTransform(scaleX: 1.2, y: 1.2)
}
animator.addAnimations({
self.animatableView.transform = CGAffineTransform.identity
}, delayFactor: 0.5)
animator.startAnimation()
Or you can try with UILayer animations
Try using UILayer animations
let layerAnimation = CABasicAnimation(keyPath: "transform.scale")
layerAnimation.fromValue = 1
layerAnimation.toValue = 1.2
layerAnimation.isAdditive = false
layerAnimation.duration = CFTimeInterval(1)
layerAnimation.fillMode = kCAFillModeForwards
layerAnimation.isRemovedOnCompletion = true
layerAnimation.repeatCount = .infinity
layerAnimation.autoreverses = true
self.animatableView.layer.add(layerAnimation, forKey: "growingAnimation")

Related

How to make UIView keyframe animation be linear from start to finish [duplicate]

I'm playing around with custom and interactive view controller transistion, with UIPercentDrivenInteractiveTransition. I'm building an app that present a card (other view controller) modally. I've made custom transistions with UIViewControllerAnimatedTransitioning that is animating the card view controller a lot like the standart model presentation style.
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
{...}
let fullScreenFrame = transitionContext.finalFrame(for: cardVC)
let offsetFrame = cardVC.view.frame.offsetBy(dx: 0, dy: cardVC.view.frame.height)
let finalFrame = presenting ? fullScreenFrame : offsetFrame
cardVC.view.frame = presenting ? offsetFrame : fullScreenFrame
containerView.addSubview(cardVC.view)
UIView.animateKeyframes(
withDuration: transitionDuration(using: transitionContext),
delay: 0,
options: UIViewKeyframeAnimationOptions.calculationModeLinear,
animations: {
UIView.addKeyframe(
withRelativeStartTime: 0,
relativeDuration: 1,
animations: { [weak self] in
guard let strongSelf = self else { return }
backgroundView.alpha = strongSelf.presenting ? 1 : 0
})
UIView.addKeyframe(
withRelativeStartTime: 0,
relativeDuration: 1,
animations: {
cardVC.view.frame = finalFrame
})
}, completion: { finished in
backgroundView.removeFromSuperview()
gradientView.alpha = 1
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
And then I use a pan gesture recognizer to interactively drive the dismiss animation:
func handleGesture(_ gestureRecognizer: UIScreenEdgePanGestureRecognizer) {
let translation = gestureRecognizer.translation(in: gestureRecognizer.view)
var progress = (translation.y / (UIScreen.main.bounds.height - 70))
progress = CGFloat(fminf(fmaxf(Float(progress), 0.0), 1.0))
switch gestureRecognizer.state {
case .began:
interactionInProgress = true
viewController.dismiss(animated: true, completion: nil)
case .changed:
shouldCompleteTransition = progress > 0.35
update(progress)
case .cancelled:
interactionInProgress = false
cancel()
case .ended:
interactionInProgress = false
if !shouldCompleteTransition {
cancel()
} else {
finish()
}
default:
print("Unsupported")
}
}
When I dismiss the presented view controller by dragging it down, it doesn't seem to move linearly with the gesture. It seems like the Interaction Controller is using some easeInEaseOut function. Maybe setting UIPercentDrivenInteractiveTransition's timingCurve to make the transition run linearly. Is that posible, or am I getting something wrong?
You yourself have given the animation a default ease in - ease out timing curve, by not setting the timing curve to anything different in your call to UIView.animateKeyframes. That applies even during interaction.
Note that setting the animation's options to UIViewKeyframeAnimationOptions.calculationModeLinear does not change the timing curve (in case that's what you thought you were accomplishing here). The way to add a linear timing curve to a linear calculation mode keyframe animation is like this:
var opts : UIViewKeyframeAnimationOptions = .calculationModeLinear
let opt2 : UIViewAnimationOptions = .curveLinear
opts.insert(UIViewKeyframeAnimationOptions(rawValue:opt2.rawValue))
// and now use `opts` as your `options`
I was having the same issue, was able to set a custom animation curve for animateKeyframes using UIView.setAnimationCurve like this:
UIView.animateKeyframes(withDuration: 4, delay: 0, options: [], animations: {
UIView.setAnimationCurve(.linear)
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.24, animations: {
// Animations...
})
// Other key frames...
})
In my case, It works with interruptibleAnimator(using:) by return UIViewPropertyAnimator

How can I animate the frame and alpha of a UIButton's titleLabel simultaneously?

I would like animate the width and alpha of my UIButton's titleLabel. The alpha animation should start after 1 second.
I'm trying to do it with Core Animation:
let animSize = CAKeyframeAnimation(keyPath: "bounds")
animSize.values = [btn1.layer.frame,CGRect(origin: btn1.layer.frame.origin, size: CGSize(width: 300, height: btn1.layer.frame.height))]
animSize.duration = 3
animSize.isRemovedOnCompletion = false
animSize.fillMode = kCAFillModeForwards
btn1.layer.add(animSize, forKey: "bounds")
let animAlpha = CABasicAnimation(keyPath: "opacity")
animAlpha.fromValue = 0.0
animAlpha.toValue = 1.0
animAlpha.timeOffset = 1.0
animAlpha.isRemovedOnCompletion = false
animAlpha.fillMode = kCAFillModeForwards
btn1.titleLabel?.layer.add(animAlpha, forKey: "opacity")
When I run the animations separately, everything's working, but when I run them together, only the first animation ("bounds") is evaluating. I know I can do it with UIView.animate but in that case I will lose the "delay" option. I've also tried to put those animations in a CAAnimationGroup but nothing changed. I want to run those animations simultaneously therefore I can't use a completion block.
Thank you for any advice.
You won't lose the "delay" option with UIView.animate. You can use two animation blocks to achieve the same result:
UIView.animate(withDuration: 3) {
btn1.frame = CGRect(origin: btn1.frame.origin, size: CGSize(width: 300, height: btn1.frame.height))
}
UIView.animate(withDuration: 2, delay: 1, options: [], animations: {
btn1.alpha = 1
}, completion: nil)
You may also try it with autolayout by hooking the button's width constraint as IBOutlet
btn1.withCon.constant = 300
UIView.animate(withDuration: 3) {
self.view.layoutIfNeeded()
}
UIView.animate(withDuration: 2, delay: 1, options: [], animations: {
btn1.alpha = 1
}, completion: nil)

Animating a UIView's alpha in sequence with UIViewPropertyAnimator

I have a UIView that I want to reveal after 0.5 seconds, and hide again after 0.5 seconds, creating a simple animation. My code is as follows:
let animation = UIViewPropertyAnimator.init(duration: 0.5, curve: .linear) {
self.timerBackground.alpha = 1
let transition = UIViewPropertyAnimator.init(duration: 0.5, curve: .linear) {
self.timerBackground.alpha = 0
}
transition.startAnimation(afterDelay: 0.5)
}
animation.startAnimation()
When I test it out, nothing happens. I assume it's because they're both running at the same time, which would mean they cancel each other out, but isn't that what the "afterDelay" part should prevent?
If I run them separately, i.e. either fading from hidden to visible, or visible to hidden, it works, but when I try to run them in a sequence, it doesn't work.
My UIView is not opaque or hidden.
You can use Timer, and add appearing / hiding animations blocks on every timer tick to your UIViewPropertyAnimatorobject.
Here's a codebase:
#IBOutlet weak var timerBackground: UIImageView!
private var timer: Timer?
private var isShown = false
private var viewAnimator = UIViewPropertyAnimator.init(duration: 0.5, curve: .linear)
override func viewDidLoad() {
super.viewDidLoad()
viewAnimator.addAnimations {
self.timerBackground.alpha = 1
}
viewAnimator.startAnimation()
isShown = true
self.timer = Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(self.startReversedAction), userInfo: nil, repeats: true)
}
func startReversedAction() {
// stop the previous animations block if it did not have time to finish its movement
viewAnimator.stopAnimation(true)
viewAnimator.addAnimations ({
self.timerBackground.alpha = self.isShown ? 0 : 1
})
viewAnimator.startAnimation()
isShown = !isShown
}
I've implemented the very similar behavior for dots jumping of iOS 10 Animations demo project.
Please, feel free to look at it to get more details.
Use UIView.animateKeyframes you'll structure your code nicely if you have complicated animations. If you'll use UIView animations nested within the completion blocks of others, it will probably result in ridiculous indentation levels and zero readability.
Here's an example:
/* Target frames to move our object to (and animate)
or it could be alpha property in your case... */
let newFrameOne = CGRect(x: 200, y: 50, width: button.bounds.size.width, height: button.bounds.size.height)
let newFrameTwo = CGRect(x: 300, y: 200, width: button.bounds.size.width, height: button.bounds.size.height)
UIView.animateKeyframes(withDuration: 2.0,
delay: 0.0,
options: .repeat,
animations: { _ in
/* First animation */
UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.5, animations: { [weak self] in
self?.button.frame = newFrameOne
})
/* Second animation */
UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.5, animations: { [weak self] in
self?.button.frame = newFrameTwo
})
/* . . . */
}, completion: nil)
What worked for me, was using sequence of UIViewPropertyAnimators. Here is example of my code:
let animator1 = UIViewPropertyAnimator(duration:1, curve: .easeIn)
animator1.addAnimations {
smallCoin.transform = CGAffineTransform(scaleX: 4, y: 4)
smallCoin.center = center
}
let animator2 = UIViewPropertyAnimator(duration:1, curve: .easeIn)
animator2.addAnimations {
center.y -= 20
smallCoin.center = center
}
let animator3 = UIViewPropertyAnimator(duration:10, curve: .easeIn)
animator3.addAnimations {
smallCoin.alpha = 0
}
animator1.addCompletion { _ in
animator2.startAnimation()
}
animator2.addCompletion { _ in
animator3.startAnimation()
}
animator3.addCompletion ({ _ in
print("finished")
})
animator1.startAnimation()
You can even add afterdelay attribute to manage speed of animations.
animator3.startAnimation(afterDelay: 10)

animateKeyframes with repeat and delay does not work as expected

I'm creating a simple left to right animation for a label using key frames but when the animation repeats, the delay is ignored.
The first time it executes, the delay of 3 seconds has an effect, but when the animation repeats, the delay is ignored. This causes the animation to re-start immediately after it ends.
UIView.animateKeyframes(withDuration: 10, delay: 3, options: [.calculationModePaced, .repeat], animations: {
let xDist = self.Label_ArtistAlbum2.frame.origin.x
UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.1, animations: {
self.Label_ArtistAlbum2.frame.origin.x = self.Label_ArtistAlbum2.frame.origin.x - (xDist * 0.1)
})
UIView.addKeyframe(withRelativeStartTime: 0.9, relativeDuration: 0.1, animations: {
self.Label_ArtistAlbum2.frame.origin.x = 0
})
}, completion: nil)
I've tried adding an extra keyframe at the end however this has no effect even with the altered times:
UIView.animateKeyframes(withDuration: 10, delay: 3, options: [.calculationModePaced, .repeat], animations: {
let xDist = self.Label_ArtistAlbum2.frame.origin.x
UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.1, animations: {
self.Label_ArtistAlbum2.frame.origin.x = self.Label_ArtistAlbum2.frame.origin.x - (xDist * 0.1)
})
UIView.addKeyframe(withRelativeStartTime: 0.1, relativeDuration: 0.7, animations: {
self.Label_ArtistAlbum2.frame.origin.x = 0
})
//attempted pause - does not appear to work perhaps since the position is unchanged?
UIView.addKeyframe(withRelativeStartTime: 0.8, relativeDuration: 0.2, animations: {
self.Label_ArtistAlbum2.frame.origin.x = 0
})
}, completion: nil)
If the delay will not be repeated along with the rest of the animation, how can I create a pause before the entire animation repeats?
I had a similar problem for animating a loading view. I solved it this way:
I created an enum for the steps in the animation
private enum TriangleToAnimate {
case one
case two
case three
case pause
}
I have my variables
private var triangleViewToFireCount = TriangleToAnimate.one
var triangle1View : TriangleView
var triangle2View : TriangleView
var triangle3View : TriangleView
I start a timer to run each animation
override init(frame: CGRect) {
Timer.scheduledTimer(timeInterval: 0.33, target: self, selector: #selector(LoadingView.timerFire), userInfo: nil, repeats: true)
}
For the selector I have a fire method. In the method I have a switch for each of the enum cases
func timerFire(){
let anim = createAnimation()
switch triangleViewToFireCount {
case .one:
triangle1View.layer.add(anim, forKey: "transform")
triangleViewToFireCount = .two
case .two:
triangle2View.layer.add(anim, forKey: "transform")
triangleViewToFireCount = .three
case .three:
triangle3View.layer.add(anim, forKey: "transform")
triangleViewToFireCount = .pause
default:
triangleViewToFireCount = .one
}
}
This is the code how I created the animation as a keyframe
func createAnimation() -> CAKeyframeAnimation{
let tr = CATransform3DIdentity
let orignalScale = CATransform3DScale(tr, 1, 1, 1)
let doubleScale = CATransform3DScale(tr, 2, 2, 1)
let keyAn = CAKeyframeAnimation(keyPath: "transform")
keyAn.keyTimes = [0, 0.1, 0.6]
keyAn.duration = 1
keyAn.values = [orignalScale,doubleScale,orignalScale]
keyAn.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
return keyAn
}
I've done a lot of testing on this issue and have found what I believe is the problem.
I think that the animation option for the Curve is my issue.
In my case choosing
calculationModePaced
has the effect of recalculating my keyframe parameters and not guaranteeing to hit any of them except the beginning and end. All intermediate keyframes become 'suggestions'.
You can't create a pause at the end because the keyframe is 'consumed' when recalculating and does not stand on it's own.
I changed to calculationModeLinear and got the keyframes I expected, and the pause too.
However, it is as smooth as I would like so I'll have to keep tinkering...
Apple's docs are here - they're descriptive but could really use some graphics and/or examples:
https://developer.apple.com/reference/uikit/uiview?language=swift
A good graph of the curves can be found here: https://www.shinobicontrols.com/blog/ios7-day-by-day-day-11-uiview-key-frame-animations

How to use UIViewPropertyAnimator to rotate 360º

If I want to take advantage of user-interactive animations via UIViewPropertyAnimator how do I animate an object 360º clockwise?
// Does not animate
let animator = UIViewPropertyAnimator(duration: 2, curve: .linear) {
let radians = Angle(360).radians // 6.28318530717959
view.transform = view.transform.rotated(by: CGFloat(radians))
}
// (Using RxSwift to handle the user interaction via UISlider,
// but that can be ignored as an implementation detail for this question.)
let scrubs = slider.rx.value.asDriver()
scrubs.drive(onNext: { (value:Float) -> Void in
animator.fractionComplete = CGFloat(value)
})
Fortunately, UIViewPropertyAnimator makes this very simple (if not slightly hackish?). Just make two animations, each for 180º.:
let animator = UIViewPropertyAnimator(duration: 2, curve: .linear) {
let radians = Angle(180).radians // 3.14159265358979
view.transform = view.transform.rotated(by: CGFloat(radians))
}
animator.addAnimations {
let radians = Angle(180).radians
view.transform = view.transform.rotated(by: radians)
}
This will act like one animation which can be started, stopped, paused, finished or "scrubbed" via fractionComplete.

Resources