How can I repeat animation (using UIViewPropertyAnimator) certain number of times?
Asked Answered
W

6

16

I want to achieve pulsation effect for the button, and hence need to repeat the spring effect several number of times, the issue is that I can't find any information about what parameters to provide and how to do it

let btnView = sayWordBtn.viewWithTag(0)
    btnView.transform = CGAffineTransform(scaleX: 0.7, y: 0.7)
    let mass: CGFloat = 2.0 // weight of the object
        let stiffness: CGFloat = 25.0 //elasticity
        let damping: CGFloat = 2*sqrt(mass*stiffness) // point where the system comes to rest in the shortest period of time
        let underDamping: CGFloat = damping * 0.5
        let initialVelocity: CGVector = CGVector.zero
        let springParameters: UISpringTimingParameters = UISpringTimingParameters(mass: mass, stiffness: stiffness, damping: underDamping, initialVelocity: initialVelocity)
        let animationDelay = 3

        let pulseEffect = UIViewPropertyAnimator(duration: 5, timingParameters: springParameters)
        pulseEffect.addAnimations( {[weak self] in
          btnView!.transform = CGAffineTransform(scaleX: 1.0, y: 1.0)
          })
        pulseEffect.isReversed = true
        pulseEffect.startAnimation(afterDelay: TimeInterval(animationDelay))
Wary answered 11/9, 2016 at 16:1 Comment(0)
Q
13

iOS 10 introduces a new object called UIViewPropertyAnimator. You can do the same things you did with UIView.animation but for achieving more complex animations, apple provides us CoreAnimation framework.

Why we should use it?

Because UIViewPropertyAnimator is a well-written and highly-extensible API. It covers a lot of the same functionality as the ‘old-style’ UIView animations, but gives you fine-grained programmatic control over the animation. This means you can pause an in-progress animation, restart it at a later date if you wish and even dynamically modify the animatable properties (such as changing the animation’s end point to the top-right of the screen when it was previously the bottom-left). We create an instance of it and create an animation as follows:

let view = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
view.alpha = 0.35 
view.backgroundColor = .red
self.view.addSubview(view)
let newFrame = CGRect(x: 0, y: 0, width: 50, height: 50)
let viewFrameAnimator = UIViewPropertyAnimator(duration: 1.0, 
                                                  curve: .linear, 
                                             animations: { view.frame = newFrame })

Now we can interacting with the animations using:

viewFrameAnimator.startAnimation..
viewFrameAnimator.stopAnimation..
viewFrameAnimator.finishAnimation(at:..

to continue the example above:

viewFrameAnimator.startAnimation()

Then, to return to alpha equal to 1, if we need more animations options, we can also launch this UIViewPropertyAnimator open class:

let viewAlphaAnimator = UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 1.0, 
                                                       delay: 0.0,
                                                     options: [.curveLinear],
                                                  animations: { view.alpha = 1.0 })

let give us to add more animation options. Now you know more than this new object.

How to repeat it?

So let's start to explain how to repeat the animation, for example if we want to repeat the view.alpha = 1.0 for 3 times (you can see the little flash light for each repeation to output..) you should follow:

let viewAlphaAnimator = UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 1.0,
                                                delay: 0.0,
                                                options: [.curveLinear,.repeat],
                                                animations: { UIView.setAnimationRepeatCount(3);view.alpha = 1.0 })

I hope this can help you.

Quack answered 20/1, 2017 at 14:36 Comment(3)
Doesn't the fact that you had to send UIView.setAnimationRepeatCount(), basically a class side global setting from the old API, kind of indicate that this new framework is not actually much better or more powerful than the old one? Perhaps more succinct for simple cases, but that doesn't strike me as elegant or powerful. If UIViewPropertyAnimator had a repeatCount like other animation objects, that would be the "more correct" thing, IMO.Neuralgia
setAnimationRepeatCount is deprecated. Now how do we repeat it?Blakeblakelee
Apple starts to put these options in blocks, try to looking for options: [.autoreverse, .curveEaseIn, .repeat]...Quack
L
5

'setAnimationRepeatCount' was deprecated in iOS 13.0. Options: [.autoreverse, .repeat] is not working too.

var animator: UIViewPropertyAnimator!

func start(_ reversed: Bool = false) {
    animator = UIViewPropertyAnimator()
    animator.addAnimations {
        //
    }
    animator.addCompletion { _ in
        start(!reversed)
    }
    animator.startAnimation()
}

func stop() {
    animator.stopAnimation(true)
}
Lucero answered 10/1, 2020 at 15:46 Comment(1)
This is awesome! Helped me stop an animation far quicker. The old way of animating actually delayed my UI refresh by 3 full seconds (lots of short animations ~0.05s each chained together for a back&forth-effect)! This one is instant :)Fled
M
3

For whatever reason they left out the ability to specify a repeat count with UIPropertyAnimator. You are left with he following options:

  1. Keep a counter and repeat the animation in the completion handler and decrement the counter until the counter reaches 0
  2. Animate with an indefinite repeat but schedule a timer and halt the animation (optionally add an animation to move to the final position)
  3. Adjust your spring parameters so that there is less dampening and more elasticity and you will get more oscillations.
  4. Use the old UIView.animate methods along with the old UIView.setAnimationRepeatCount
  5. Use CASpringAnimation since thats what the Property animator is wrapping anyways, and it does have a repeat count.
Maim answered 19/1, 2017 at 6:1 Comment(0)
D
2

Please use

    pulseEffect.addCompletion({_ in
            // Repeat the logic here
    })

You can repeat the animation logic inside the addCompletion block

func animateView() {
    btnView.transform = CGAffineTransform(scaleX: 0.7, y: 0.7)
    let mass: CGFloat = 2.0 // weight of the object
    let stiffness: CGFloat = 25.0 // elasticity
    let damping: CGFloat = 2*sqrt(mass*stiffness) // point where the system comes to rest in the shortest period of time
    let underDamping: CGFloat = damping * 0.5
    let initialVelocity: CGVector = CGVector.zero
    let springParameters: UISpringTimingParameters = UISpringTimingParameters(mass: mass, stiffness: stiffness, damping: underDamping, initialVelocity: initialVelocity)
    let animationDelay = 3

    let pulseEffect = UIViewPropertyAnimator(duration: 5, timingParameters: springParameters)
    pulseEffect.addAnimations( {[weak self] in
        self?.btnView!.transform = CGAffineTransform(scaleX: 1.0, y: 1.0)
        })
    pulseEffect.addCompletion({_ in
        self.animateView()
    })
    pulseEffect.isReversed = true
    pulseEffect.startAnimation(afterDelay: TimeInterval(animationDelay))
}

You can also add the logic to restrict the animation to specific number in the addCompletion block

Dialogue answered 17/1, 2017 at 5:54 Comment(3)
Won't this eventually cause an, ahem, "Stack Overflow"?Chromolithography
@gmw, there is no stack overflow because this is not recursive. The completion block is not run at the end of this function, it is queued and/or called after this function returns (and releases its stack space).Blakeblakelee
This code repeats the animation indefinitely but it also makes any calls to UIViewPropertyAnimator.stopAnimation(true) meaningless, no? So the animation cannot be stopped... At least this is the problem I'm now encounteringApostasy
W
0

Please use this modified code

let btnView = sayWordBtn.viewWithTag(0)
btnView.transform = CGAffineTransform(scaleX: 0.7, y: 0.7)
let mass: CGFloat = 2.0 // weight of the object
let stiffness: CGFloat = 25.0 //elasticity
let damping: CGFloat = 2*sqrt(mass*stiffness) // point where the system comes to rest in the shortest period of time
let underDamping: CGFloat = damping * 0.5
let initialVelocity: CGVector = CGVector.zero
let springParameters: UISpringTimingParameters = UISpringTimingParameters(mass: mass, stiffness: stiffness, damping: underDamping, initialVelocity: initialVelocity)
let animationDelay = 3

let pulseEffect = UIViewPropertyAnimator(duration: 5, timingParameters: springParameters)
pulseEffect.addAnimations( {[weak self] in
    UIView.setAnimationRepeatCount(3)
    UIView.setAnimationRepeatAutoreverses(true)
    btnView!.transform = CGAffineTransform(scaleX: 1.0, y: 1.0)
})
pulseEffect.isReversed = true
pulseEffect.startAnimation(afterDelay: TimeInterval(animationDelay))
Warring answered 23/1, 2017 at 7:31 Comment(0)
T
0

This code works:

func usePropertyAnimator(_ reversed: Bool = false) {

    let circleAnimator = UIViewPropertyAnimator(
        duration: 2, timingParameters: UICubicTimingParameters())

    circleAnimator.addAnimations {
        circle.center.y += (reversed ? -1 : 1) * 330
    }

    circleAnimator.addCompletion { _ in
        usePropertyAnimator(!reversed)
    }

    circleAnimator.startAnimation()

}

usePropertyAnimator()
Turnabout answered 23/2, 2017 at 9:53 Comment(1)
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Attempt to add an animation to an animator <UIViewPropertyAnimator(0x6000001f4000) [unknown] running interruptible> as it is completing'Probabilism

© 2022 - 2024 — McMap. All rights reserved.