为 CALayer 设置动画的安全方法

safe way to animate CALayer

当我在寻找 CALayer 动画时,我找到了这样的解决方案:

let basicAnimation = CABasicAnimation(keyPath: "opacity")
basicAnimation.fromValue = 0
basicAnimation.toValue = 1
basicAnimation.duration = 0.3
add(basicAnimation, forKey: "opacity")

但是fromValue和toValue都是Any类型,我们可以使用任何字符串作为键,这是不安全的。是否有使用最新 Swift 功能的更好方法?

我想出了使用非常简单的解决方案:

layer.animate(.init(
    keyPath: \.opacity,
    value: "1", // this will produce an error
    duration: 0.3)
)
layer.animate(.init(
    keyPath: \.opacity,
    value: 1, // correct
    duration: 0.3)
)
layer.animate(.init(
    keyPath: \.backgroundColor,
    value: UIColor.red, // this will produce an error
    duration: 0.3,
    timingFunction: .init(name: .easeOut),
    beginFromCurrentState: true)
)
layer.animate(.init(
    keyPath: \.backgroundColor,
    value: UIColor.red.cgColor, // correct
    duration: 0.3,
    timingFunction: .init(name: .easeOut),
    beginFromCurrentState: true)
)

解决方案代码为:

import QuartzCore

extension CALayer {
    struct Animation<Value> {
        let keyPath: ReferenceWritableKeyPath<CALayer, Value>
        let value: Value
        let duration: TimeInterval
        let timingFunction: CAMediaTimingFunction? = nil
        let beginFromCurrentState = false
    }
    
    @discardableResult func animate<Value>(
        _ animation: Animation<Value>,
        completionHandler: (() -> Void)? = nil)
    -> CABasicAnimation?
    {
        CATransaction.begin()
        CATransaction.setCompletionBlock(completionHandler)
        defer {
            // update actual value with the final one
            self[keyPath: animation.keyPath] = animation.value
            CATransaction.commit()
        }
        guard animation.duration > 0 else { return nil }
        let fromValueLayer: CALayer
        if animation.beginFromCurrentState, let presentation = presentation() {
            fromValueLayer = presentation
        } else {
            fromValueLayer = self
        }
        let basicAnimation = CABasicAnimation(
            keyPath: NSExpression(forKeyPath: animation.keyPath).keyPath
        )
        basicAnimation.timingFunction = animation.timingFunction
        basicAnimation.fromValue = fromValueLayer[keyPath: animation.keyPath]
        basicAnimation.toValue = animation.value
        basicAnimation.duration = animation.duration
        
        add(basicAnimation, forKey: basicAnimation.keyPath)
        return basicAnimation
    }
}

优点:

  • CALayer 上可用的 keyPath 自动补全
  • 值类型取决于 keyPath,因此您将无法设置错误
  • 清码

缺点:

  • 我们仍然可以选择非动画 keyPath