UIViewPropertyAnimator isReversed未更新视图transform问题求助
我需要实现如下视图动画:
- 使用
CGAffineTransform将视图的Y轴缩放至0 - 修改视图背景色并将视图
transform恢复为.identity
为复用同一缓动函数,我希望仅使用一个UIViewPropertyAnimator并利用其isReversed属性,但尝试后发现:屏幕显示效果正常,但视图内部的Y轴缩放值仍为0。
代码执行后,打印显示Y轴缩放值为0,但屏幕上显示的是200px的蓝色正方形,视图检查结果也显示缩放值为0。请问我哪里操作有误?
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() } }
核心问题
当你设置isReversed = true并重新启动动画时,UIViewPropertyAnimator只是反向播放已添加的动画(从y=1e-10回到初始的y=1),但动画完成后并不会自动将视图的transform设置回.identity。因为你最初添加的动画目标是y=1e-10,反向动画的“起点”是这个值,终点是动画开始前的初始值(也就是.identity),但由于你设置了pausesOnCompletion = true,动画完成后停在终点状态,但UIViewPropertyAnimator的内部状态并没有更新,导致直接读取testView.transform.d时,拿到的是动画块中设置的目标值1e-10,而非实际显示的.identity。
另外,你使用isRunning的publisher监听动画状态的时机存在问题:第一次动画完成(缩放至y=1e-10)时isRunning变为false,此时切换isReversed并重启动画;第二次动画完成时isRunning再次变为false,此时需要手动同步属性值与视觉状态。
解决方案
方案1:反向动画完成后手动复位transform
修改sink逻辑,在反向动画结束时直接将transform设置为.identity,强制同步属性值与视觉状态:
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 } if !self.scaleAnimator.isReversed { print("Half animation") print("Scale Y : \(self.testView.transform.d)") self.testView.backgroundColor = .blue self.scaleAnimator.isReversed = true self.scaleAnimator.startAnimation() } else { print("Animation end") // 手动复位transform,同步属性值与视觉状态 self.testView.transform = .identity print("Scale Y : \(self.testView.transform.d)") } } .store(in: &cancellables)
方案2:更新动画目标值实现反向效果
在第一次动画完成后,移除原有动画并添加目标为.identity的新动画,避免依赖isReversed的状态问题:
func setupAnimation() { scaleAnimator.pausesOnCompletion = true // 初始动画:缩放到y=1e-10 scaleAnimator.addAnimations { self.testView.transform = CGAffineTransform(scaleX: 1, y: 1e-10) } 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 } if !self.scaleAnimator.isReversed { print("Half animation") print("Scale Y : \(self.testView.transform.d)") self.testView.backgroundColor = .blue // 停止当前动画并更新目标值 self.scaleAnimator.stopAnimation(true) self.scaleAnimator.addAnimations { self.testView.transform = .identity } self.scaleAnimator.startAnimation() } else { print("Animation end") print("Scale Y : \(self.testView.transform.d)") } } .store(in: &cancellables) }
方案3:提取缓动函数用关键帧动画实现(可选)
如果复用缓动函数是核心需求,可以将缓动参数提取为独立对象,在两次动画中复用:
let timingParameters = UICubicTimingParameters(controlPoint1: CGPoint(x: 0.36, y: 0), controlPoint2: CGPoint(x: 0.66, y: -0.56)) // 第一阶段:缩放到Y=0 UIView.animate(withDuration: 2, delay: 0, timingParameters: timingParameters) { self.testView.transform = CGAffineTransform(scaleX: 1, y: 1e-10) } completion: { _ in self.testView.backgroundColor = .blue // 第二阶段:恢复到原尺寸 UIView.animate(withDuration: 2, delay: 0, timingParameters: timingParameters) { self.testView.transform = .identity } completion: { _ in print("Final Scale Y: \(self.testView.transform.d)") } }
关键说明
UIViewPropertyAnimator的isReversed仅改变播放方向,不会自动修改动画的目标值。设置pausesOnCompletion = true时,动画完成后视觉状态停在终点,但属性值可能因animator内部状态未更新而显示为初始动画的目标值,这是UIKit动画机制导致的临时异步问题。- 手动设置
transform = .identity是最直接的同步方式,能解决打印和视图检查器显示不一致的问题。
内容的提问来源于stack exchange,提问作者Xys

