iosswiftcalayerios-animations

safe way to animate CALayer


When I was looking for a CALayer animation, I found solutions like this one:

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

But fromValue and toValue are type of Any, and as a key we can use any string, which is not safe. Is there a better way to do so using newest Swift features?


Solution

  • I came up with the solution where usage is pretty simple:

    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)
    )
    

    And the solution code is:

    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
        }
    }
    

    Pros:

    Cons: