Search code examples
animationios13xcode11

Why Is the Final Presentation of the Animation's Result Not What Is Expected?


The following gif demonstrates an animation issue that I am having. The small, red view is being animated. The small, green view marks the initial position. The red view is animated down and to the right, and is then auto-reversed. At the end, after returning to the initial position, the red view jumps to the left: it is this jump that I do not understand.

enter image description here

Here is the code. The animation has two parts: an initial change to the Y position and an X position change that kicks in half way through. Both animations are auto-reversed. Note the completion closure that updates the model to reflect the final view position which was brought about by the auto-reversal. It all seems correct to me. Why the jump to the left?

    @objc private func animate() {

        let center = animationView.center

        let distance: CGFloat = 100
        let down = {
            UIView.setAnimationRepeatAutoreverses(true)
            self.animationView.center.y += distance
        }
        let right = {
            UIView.setAnimationRepeatAutoreverses(true)
            self.animationView.center.x += distance
        }

        let animator = UIViewPropertyAnimator(duration: 2, curve: .linear, animations: down)
        animator.addAnimations(right, delayFactor: 0.5)
        animator.addCompletion { _ in self.animationView.center = center } // Sync up with result of the autoreverse
        animator.startAnimation()
    }
  1. The jump to the left appears to be 100 points (same as the animated position change)
  2. The problem seems to be related to the second part of the animation. If I do not add it and instead modify X in the first part (where Y is modified) then all is well.
  3. When, after the animation completes, I examine the red view's center I find that it is "correct"; it is equal to the root view's center. In other words: the model and the presentation do not match???
  4. When I use Xcode's Debug View Hierarchy capability the red view is depicted as being in the center of the screen

// ***********************************************************************

Update #1: The animation consists of two parts; down and right. In each of those two parts I am invoking UIView.setAnimationRepeatAutoreverse. If I comment out either or both of those auto reversals then everything works as expected (albeit I do not get the effect of the auto-reversal, but the behavior of the code is understandable).

// ***********************************************************************

Update #2: From Apple's documentation of UIView.setAnimationRepeatAutoreverse: Use of this method is discouraged in iOS 4.0 and later. Instead, you should use the animate(withDuration:delay:options:animations:completion:) method to specify your animations and the animation options.

Okay. Let's see what else is available to accomplish the delay of the second part of the animation.


Solution

  • At last!

    private func animate() {
    
        let distance: CGFloat = 100
        let down = { self.animationView.center.y += distance }
        let right = { self.animationView.center.x += distance }
    
        let animator = UIViewPropertyAnimator(duration: 2, curve: .linear, animations: down)
        animator.addAnimations(right, delayFactor: 0.5)
        animator.pausesOnCompletion = true
        let observer = animator.observe(\.isRunning, options: [.new] ) { animator, change in
            print("isRunning changed to \(change.newValue!).")
    
            if !animator.isRunning {
                if !animator.isReversed {
                    print("Reversing animation")
                    animator.isReversed = true;
                    animator.startAnimation()
                }
                else {
                    print("Stopping animation")
                    animator.stopAnimation(false)
                    animator.finishAnimation(at: .start)
                }
            }
        }
        animator.addCompletion { position in
            print("Animation completed at \(position). State = \(animator.state).")
            observer.invalidate() // By capturing the observer here in the completion closure we keep it alive for the duration of the animation.
        }
    
        print("\nStarting animation")
        animator.startAnimation()
        print("Animation started")
    }