Search code examples
iosswiftuiviewpropertyanimator

Synchronously animate CALayer property and UIView with UIViewPropertyAnimator


I'm using a UIViewPropertyAnimator to animate the position of a view. But, I also want to animate CALayer properties like borderColor along with it. Currently, it doesn't animate and instantly changes at the start, like this:

GIF of a square moving right while the border instantly changes color

Here's my code:

class ViewController: UIViewController {
    var animator: UIViewPropertyAnimator?
    let squareView = UIView(frame: CGRect(x: 0, y: 40, width: 80, height: 80))
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.addSubview(squareView)
        
        squareView.backgroundColor = UIColor.blue
        squareView.layer.borderColor = UIColor.green.cgColor
        squareView.layer.borderWidth = 6
        
        animator = UIViewPropertyAnimator(duration: 2, curve: .easeOut, animations: {
            self.squareView.frame.origin.x = 100
            self.squareView.layer.borderColor = UIColor.red.cgColor
        })
        
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            self.animator?.startAnimation()
        }
    }
}

I looked at this question, How to synchronously animate a UIView and a CALayer, and the answer suggested using "an explicit animation, like a basic animation." It said,

If the timing function and the duration of both animations are the same then they should stay aligned.

However, if I use CABasicAnimation, I lose all the benefits of UIViewPropertyAnimator, like timing and stopping in the middle. I will also need to keep track of both. Is there any way to animate CALayer properties with UIViewPropertyAnimator?


Solution

  • CABasicAnimation has timing as well as keyframe animation to stop in the middle. But, to replicate your animation above:

    squareView.layer.transform = CATransform3DMakeTranslation(100, 0, 0)
    let fromValue = squareView.transform
    let toValue = 100
    let translateAnimation = CABasicAnimation(keyPath: #keyPath(CALayer.transform))
    translateAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
    translateAnimation.fromValue = fromValue
    translateAnimation.toValue = toValue
    translateAnimation.valueFunction = CAValueFunction(name: .translateX)
    
    let fromColor = squareView.layer.borderColor
    let toColor = UIColor.red.cgColor
    let borderColorAnimation = CABasicAnimation(keyPath: "borderColor")
    borderColorAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
    borderColorAnimation.fromValue = fromColor
    borderColorAnimation.toValue = toColor
    
    let groupAnimation = CAAnimationGroup()
    groupAnimation.animations = [translateAnimation, borderColorAnimation]
    groupAnimation.duration = 5
    squareView.layer.add(groupAnimation, forKey: nil)