UIViewPropertyAnimator isReversed doesn't update view transform

56 Views Asked by At

I have a view that I want to animate the following way :

  • Change view transform Y scale down to 0 using CGAffineTransform
  • Change view background color and scale view transform back to .identity

Since I want to reuse the same easing function, I would like to have only one UIViewPropertyAnimator and use its isReversed property.

However when I try this, the result seems good on screen but the internal scale stays at 0.

Here's a simple ViewController that replicates the issue :

import UIKit
import Combine

class ViewController: UIViewController {
    private var testView: UIView = {
        let view = UIView()
        view.backgroundColor = .red
        return view
    }()

    private lazy var scaleAnimator = UIViewPropertyAnimator(duration: 2,
                                                            controlPoint1: CGPoint(x: 0.36, y: 0),
                                                            controlPoint2: CGPoint(x: 0.66, y: -0.56))
    private var cancellables = Set<AnyCancellable>()

    override func viewDidLoad() {
        super.viewDidLoad()

        view.addSubview(testView)
        testView.translatesAutoresizingMaskIntoConstraints = false

        NSLayoutConstraint.activate([
            testView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            testView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            testView.widthAnchor.constraint(equalToConstant: 200),
            testView.heightAnchor.constraint(equalToConstant: 200),
        ])

        setupAnimation()

        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            self.startAnimation()
        }
    }

    func setupAnimation() {
        scaleAnimator.pausesOnCompletion = true

        scaleAnimator.addAnimations {
            self.testView.transform = CGAffineTransform(scaleX: 1, y: 1e-10) // Scale animations to exactly 0.0 don't work
        }

        scaleAnimator.publisher(for: \.isRunning, options: [.new])
            .sink { [weak self] isRunning in
                guard let self,
                      !isRunning else {
                    print("Animation start")
                    print("Scale Y : \(self!.testView.transform.d)")
                    return
                }

                guard !self.scaleAnimator.isReversed else {
                    print("Animation end")
                    print("Scale Y : \(self.testView.transform.d)")
                    return
                }

                print("Half animation")
                print("Scale Y : \(self.testView.transform.d)")
                self.testView.backgroundColor = .blue
                self.scaleAnimator.isReversed = true
                self.scaleAnimator.startAnimation()
            }
            .store(in: &cancellables)
    }

    func startAnimation() {
        scaleAnimator.isReversed = false
        scaleAnimator.startAnimation()
    }
}

At the end, this code prints that the Y scale is 0, but on screen I have a 200px blue square. If I inspect the view, its scale is 0.

What am I doing wrong here ?

1

There are 1 best solutions below

2
DonMag On BEST ANSWER

If we set:

scaleAnimator.pausesOnCompletion = true

when we get to the "Animation end" in this block:

guard !self.scaleAnimator.isReversed else {
    print("Animation end")
    print("Scale Y : \(self.testView.transform.d)")
    return
}

the animator is still running.

If we change that to:

guard !self.scaleAnimator.isReversed else {
    print("Animation end")
    print("End 1 :  Scale Y : \(self.testView.transform.d)")
    self.scaleAnimator.stopAnimation(true)
    print("End 2 :  Scale Y : \(self.testView.transform.d)")
    return
}

The "blue frame" and transform should be correct.

Quick testing, and I sometimes see transform.d == 0.9999999, so you may want to do this:

    self.scaleAnimator.stopAnimation(true)
    self.testView.transform = .identity