Search code examples
swiftanimationcabasicanimationcagradientlayercatransaction

UIView replicate CAAnimation of another view, in real time?


So I've got a background view with a gradient sublayer, animating continuously to change the colors slowly. I'm doing it with a CATransaction, because I need to animate other properties as well:

CATransaction.begin()

gradientLayer.add(colorAnimation, forKey: "colors")
// other animations

CATransaction.setCompletionBlock({
    // start animation again, loop forever
}

CATransaction.commit()

Now I want to replicate this gradient animation, let's say, for the title of a button for instance.

Desired result

Note 1: I can't just "make a hole" in the button, if such a thing is possible, because I might have other opaque views between the button and the background.

Note 2: The gradient position on the button is not important. I don't want the text gradient to replicate the exact colors underneath, but rather to mimic the "mood" of the background.

So when the button is created, I add its gradient sublayer to a list of registered layers, that the background manager will update as well:

func register(layer: CAGradientLayer) {
    let pointer = Unmanaged.passUnretained(layer).toOpaque()
    registeredLayers.addPointer(pointer)
}

So while it's easy to animate the text gradient at the next iteration of the animation, I would prefer that the button starts animating as soon as it's added, since the animation usually takes a few seconds. How can I copy the background animation, i.e. set the text gradient to the current state of the background animation, and animate it with the right duration left and timing function?


Solution

  • The solution was indeed to use the beginTime property, as suggested by @Shivam Gaur's comment. I implemented it as follows:

    // The background layer, with the original animation
    var backgroundLayer: CAGradientLayer!
    
    // The animation
    var colorAnimation: CABasicAnimation!
    
    // Variable to store animation begin time
    var animationBeginTime: CFTimeInterval!
    
    // Registered layers replicating the animation
    private var registeredLayers: NSPointerArray = NSPointerArray.weakObjects()
    
    ...
    
    // Somewhere in our code, the setup function
    func setup() {
        colorAnimation = CABasicAnimation(keyPath: "colors")
        // do the animation setup here
        ...
    }
    ...
    
    // Called by an external class when we add a view that should replicate the background animation
    func register(layer: CAGradientLayer) {
    
        // Store a pointer to the layer in our array
        let pointer = Unmanaged.passUnretained(layer).toOpaque()
        registeredLayers.addPointer(pointer)
    
        layer.colors = colorAnimation.toValue as! [Any]?
    
        // HERE'S THE KEY: We compute time elapsed since the beginning of the animation, and start the animation at that time, using 'beginTime'
        let timeElapsed = CACurrentMediaTime() - animationBeginTime
        colorAnimation.beginTime = -timeElapsed
    
        layer.add(colorAnimation, forKey: "colors")
        colorAnimation.beginTime = 0
    }
    
    // The function called recursively for an endless animation
    func animate() {
    
        // Destination layer
        let toLayer = newGradient() // some function to create a new color gradient
        toLayer.frame = UIScreen.main.bounds
    
        // Setup animation
        colorAnimation.fromValue = backgroundLayer.colors;
        colorAnimation.toValue = toLayer.colors;
    
        // Update background layer
        backgroundLayer.colors = toLayer.colors
    
        // Update registered layers (iterate is a custom function I declared as an extension of NSPointerArray)
        registeredLayers.iterate() { obj in
            guard let layer = obj as? CAGradientLayer else { return }
            layer.colors = toLayer.colors
        }
    
        CATransaction.begin()
    
        CATransaction.setCompletionBlock({
            animate()
        })
    
        // Add animation to background
        backgroundLayer.add(colorAnimation, forKey: "colors")
    
        // Store starting time
        animationBeginTime = CACurrentMediaTime();
    
        // Add animation to registered layers
        registeredLayers.iterate() { obj in
            guard let layer = obj as? CAGradientLayer else { return }
            layer.add(colorAnimation, forKey: "colors")
        }
    
        CATransaction.commit()
    
    }