I'm trying to animate a sine wave made out of a Shape struct. To do this, I need to animate the phase of the wave as a continuous looping animation - and then animate the frequency and strength of the wave based on audio input.
As far as I understand it, the animatableData property on the Shape seems unable to handle multiple animation transactions. I've tried using AnimatablePair to be able to set/get more values than one, but it seems they need to come from the same animation transaction (and for my case I need two different instances).
The code goes something like this:
@Binding var externalInput: Double
@State private var phase: Double = 0.0
@State private var strength: Double = 0.0
struct MyView: View {
var body: some View {
MyShape(phase: self.phase, strength: self.strength)
.onAppear {
/// The first animation (that needs to be continuous)
withAnimation(.linear(duration: 1).repeatForever(autoreverses: false)) {
self.phase = .pi * 2
}
}
.onChange(self.externalInput) {
/// The second animation (that is reactive)
withAnimation(.default) {
self.strength = self.externalInput
}
}
}
}
struct MyShape: Shape {
var phase: Double
var strength: Double
var animatableData: AnimatablePair<Double, Double> {
get { .init(phase, strength) }
set {
phase = newValue.first
strength = newValue.second
}
}
/// Drawing the bezier curve that integrates the animated values
func path(in rect: CGRect) -> Path {
...
}
}
This approach however seems to only animate the value from the last animation transaction that's been initialised. I am guessing both animated values goes in, but when the second animation transaction fires, it sets the first value to the target value (without any animations) - as those animated values are part of the first transaction (that gets overridden by the second one).
My solution right now is to use an internal Timer, in a wrapper View for the Shape struct, to take care of updating the value of the phase value - but this is far from optimal, and quite ugly.
When setting the values in animatableData, is there a way to access animated values from other transactions - or is there another way to solve this?
I've also tried with implicit animations, but that seems to only render the last set animation as well - and with a lot of other weird things happening (like the whole view zooming across the screen on a repeated loop...).
EDIT 1:
Apparently (according to apples docs) you should be able to set a custom transaction key on the transaction (animation) - but I can't for the life of me figure out how to read that key in the animatableData setter. I was thinking maybe you could look at what kind of animation key is currently supplying data, and update either phase or strength depending on which transaction the data is coming from.
Then again, I would still have the issue of my animations cancelling each other out (so the data from the first animation would no longer be supplying data...).
EDIT 2:
I switched out the Timer in favour of a CADisplayLink, to better time the phase updates with the framerate - which fixes the performance issues.
However, I would still like to know how to set this up as a proper animation. It must be possible, as all other native structs (that implements the Animatable protocol) seem to be able to handle multiple animations. Or maybe it's a limitation of the Shape structs for some reason?