为 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
当我在寻找 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