I have the following wave animation happening and on button press I animate it's strength such that it flatlines with animation. However, when I press the button again, the wave animation doesn't restart. How do I get it to re-start the animation again?
struct Wave: Shape {
var strength: Double
var frequency: Double
var phase: Double
var animatableData: AnimatablePair<Double, Double> {
get { AnimatablePair(phase, strength) }
set {
self.phase = newValue.first
self.strength = newValue.second
}
}
func path(in rect: CGRect) -> Path {
var path = Path()
let width = Double(rect.width)
let height = Double(rect.height)
let midHeight = height / 2
let wavelength = width / frequency
let firstX = 0.0
let firstRelativeX = firstX / wavelength
let firstSine = sin(firstRelativeX + phase)
let firstY = strength * firstSine + midHeight
path.move(to: CGPoint(x: firstX, y: firstY))
for x in stride(from: 0.0, through: width, by: 1) {
let relativeX = x / wavelength
let sine = sin(relativeX + phase)
let y = strength * sine + midHeight
path.addLine(to: CGPoint(x: x, y: y))
}
return path
}
}
struct WaveView: View {
@Binding var isAnimating: Bool
@State private var phase = 0.0
@State private var waveStrength = 50.0
var body: some View {
VStack {
Wave(strength: waveStrength, frequency: 30, phase: phase)
.stroke(.black, lineWidth: 5)
.onChange(of: isAnimating) { _, newValue in
withAnimation(.easeInOut(duration: 0.5)) {
waveStrength = newValue ? 50.0 : 0.0
}
if newValue {
animateWave()
}
}
.onAppear {
animateWave()
}
}
}
private func animateWave() {
withAnimation(Animation.linear(duration: 2).repeatForever(autoreverses: false)) {
self.phase = .pi * 2
}
}
}
struct WaveContainerView: View {
@State var isAnimating = true
var body: some View {
VStack {
WaveView(isAnimating: $isAnimating)
Button("Animate") {
isAnimating.toggle()
}
}
}
}
To get your animation start again, you need to reset phase
value before start animation. After WaveView
appear, phase
value get set to .pi * 2
, then when you press button, you just set it to the same value, SwiftUI do not see any change of phase
so your animation is not run, also when onChange
closure of isAnimating
get called, a new withAnimation
block get called therefore current animation repeatForever
get cancel because animation
for phase
and strength
are stick together in AnimatablePair
:
private func animateWave() {
phase = 0 // <- reset phase value here
withAnimation(Animation.linear(duration: 2).repeatForever(autoreverses: false)) {
self.phase = .pi * 2
}
}
You can comment out withAnimation
block in onChange(of: isAnimating)
to keep repeatForever
animation, but then you will not have animation when waveStrength
change:
.onChange(of: isAnimating) { _, newValue in
//withAnimation(.easeInOut(duration: 0.5)) {
waveStrength = newValue ? 50.0 : 0.0
//}
if newValue {
animateWave()
}
}
Another fix is to seperate phase
and waveStrength
from AnimatablePair
:
struct ShapheWave: View, @preconcurrency Animatable {
var strength: Double
var frequency: Double
var phase: Double
var animatableData: Double {
get { phase }
set {
self.phase = newValue
}
}
var body: some View {
Wave(strength: strength, frequency: frequency, phase: phase)
.stroke(.black, lineWidth: 5)
}
}
struct Wave: Shape {
var strength: Double
var frequency: Double
var phase: Double = .pi * 2
var animatableData: Double {
get { strength }
set {
self.strength = newValue
}
}
func path(in rect: CGRect) -> Path {
var path = Path()
let width = Double(rect.width)
let height = Double(rect.height)
let midHeight = height / 2
let wavelength = width / frequency
let firstX = 0.0
let firstRelativeX = firstX / wavelength
let firstSine = sin(firstRelativeX + phase)
let firstY = strength * firstSine + midHeight
path.move(to: CGPoint(x: firstX, y: firstY))
for x in stride(from: 0.0, through: width, by: 1) {
let relativeX = x / wavelength
let sine = sin(relativeX + phase)
let y = strength * sine + midHeight
path.addLine(to: CGPoint(x: x, y: y))
}
return path
}
}
struct WaveView: View {
@Binding var isAnimating: Bool
@State private var phase = 0.0
@State private var waveStrength = 50.0
var body: some View {
VStack {
ShapheWave(strength: waveStrength, frequency: 30, phase: phase)
.onChange(of: isAnimating) { _, newValue in
waveStrength = newValue ? 50.0 : 0.0
}
.onAppear {
animateWave()
}
.animation(.linear(duration: 2).repeatForever(autoreverses: false), value: phase) .animation(.easeInOut(duration: 0.5), value: waveStrength)
}
}
private func animateWave() {
phase = .pi * 2
}
}