Search code examples
iosuiviewpropertyanimator

UIViewPropertyAnimator stop repeating


I have this code in a Factory class to return UIViewPropertyAnimators to be used where and when:

class AnimatorFactory {

    @discardableResult
    static func spinSpindle(spindle: UIView) -> UIViewPropertyAnimator {
        let spinSpindleAnimator = UIViewPropertyAnimator(duration: 1.0, curve: .linear)

        spinSpindleAnimator.addAnimations {
            spindle.transform = CGAffineTransform(rotationAngle: .pi)
        }
        spinSpindleAnimator.addCompletion{ position in
            print("spinSpindle.complete: \(position.rawValue)")
            spindle.transform = .identity
            self.spinSpindle(spindle: spindle)
        }
        spinSpindleAnimator.startAnimation()

        return spinSpindleAnimator
    }
...

The code creates a UIViewPropertyAnimator that allows for a perpetually animating view (one that keeps on spinning and spinning). It does this by calling itself again in the completion block. The problem I have now is that I am unable to stop the animation using UIViewPropertyAnimator.stopAnimation(true).

The true in stopAnimation(_ withoutFinishing:) value is meant to ensure that the animation is returned to the UIViewAnimatingState.inactive state without having to perform any cleanup with finishAnimation(at finalPosition:).

When I call stopAnimation(true) the animation does not stop. It is called like this:

    print("\(spindleAnimator.description)")
    spindleAnimator.stopAnimation(true)
    print("\(spindleAnimator.description)")

The above code is triggered by the user tapping on the animating view and outputs this:

<UIViewPropertyAnimator(0x600001c1c100) [inactive] interruptible>
<UIViewPropertyAnimator(0x600001c1c900) [inactive] interruptible>

To me this suggests that the spindleAnimator refers to a different UIViewPropertyAnimator due to the "recursive" call in the AnimatorFactory.spinSpindle(spindle:) static method. I might be mistaken.

Essentially I'm looking for help in order to create a truly interactive and repeating animation. So far I've been having incredible trouble with it, but I won't complain as I've learned a lot. However, I need some help tying it all together by finding a working stopAnimation(withoutFinishing:) call.

Thanks!

EDIT:

Another interesting fact is that I am able to stop the animation if I click the spindle before it completes its first revolution (in other words, before the completion block is executed). This leads me to believe that the "recursive" call to spinSpindle(spindle:) creates another UIViewPropertyAnimator for which I have no handle to call stopAnimation(withoutFinishing:). Although, I don't think I see how this is the case...

I tested this theory by printing spinSpindleAnimator.description each time the call to spinSpindle(spindle:) is made:

class AnimatorFactory {

    @discardableResult
    static func spinSpindle(spindle: UIView) -> UIViewPropertyAnimator {
        let spinSpindleAnimator = UIViewPropertyAnimator(duration: 1.0, curve: .linear)

        spinSpindleAnimator.addAnimations {
            spindle.transform = CGAffineTransform(rotationAngle: .pi)
        }
        spinSpindleAnimator.addCompletion{ position in
            print("(\(spinSpindleAnimator.description)) spinSpindle.complete: \(position.rawValue)")
            spindle.transform = .identity
            self.spinSpindle(spindle: spindle)
        }
        spinSpindleAnimator.startAnimation()

        return spinSpindleAnimator
    }

and it gives

(<UIViewPropertyAnimator(0x600001b20300) [unknown] running interruptible>) spinSpindle.complete: 0
(<UIViewPropertyAnimator(0x600001b28b00) [unknown] running interruptible>) spinSpindle.complete: 0
(<UIViewPropertyAnimator(0x600001b38600) [unknown] running interruptible>) spinSpindle.complete: 0
(<UIViewPropertyAnimator(0x600001b31000) [unknown] running interruptible>) spinSpindle.complete: 0
(<UIViewPropertyAnimator(0x600001b38300) [unknown] running interruptible>) spinSpindle.complete: 0
(<UIViewPropertyAnimator(0x600001b38600) [unknown] running interruptible>) spinSpindle.complete: 0
(<UIViewPropertyAnimator(0x600001b38700) [unknown] running interruptible>) spinSpindle.complete: 0
(<UIViewPropertyAnimator(0x600001b20600) [unknown] running interruptible>) spinSpindle.complete: 0
...

confirming my suspicions.


Solution

  • You are not able to stop animation because you are holding the first object of UIViewPropertyAnimator which is created and returned by your function, but this line of your function creating every time new object of UIViewPropertyAnimator

    self.spinSpindle(spindle: spindle)
    

    and you are not holding/returning this object to anywhere. You are always calling stopAnimation function to first object which you have returned from function. That's why you are able to stop animation before you click the spindle before it completes its first revolution and not after the completion block called.