You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 14:30:05