2

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?

bplattenburg
  • 623
  • 1
  • 8
  • 33
Morpheus
  • 1,189
  • 2
  • 11
  • 33
  • I don't see why you couldn't just add a similar animation to both the layer containing the text "HELLO" and the background layer at the back. Do you have more details of what your animation looks like? – bplattenburg Dec 02 '19 at 18:43
  • I could, if the animations started at the same time. However, while the background animation begins at the launch of the app, the button can be added at any time. Therefore my question is how could I get the current state of the background animation, so that I can add to the button an animation that is synchronized with the background, and not delayed. – Morpheus Dec 03 '19 at 08:46
  • 2
    CABasicAnimation has a beginTime property which you can use to synch your animations with others – Shivam Gaur Dec 06 '19 at 07:49
  • @Coconuts Do you have the above sample hosted in github? If so, can you share the link? – Subramanian Mariappan Dec 06 '19 at 12:40

1 Answers1

2

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

}
Morpheus
  • 1,189
  • 2
  • 11
  • 33