I want to animate multiple custom shapes and have run into the issue that I can't make subsequent shapes animate immediately after they appear for the first time.
My goal is that you can keep replacing the second shape with another shape and it immediately animates.
Basically, when the first shape appears it immediately animates as expected. The problem arises when you get the second shape in. The first one is still animating, the second one is not. You have to first remove the second shape and then toggle it back on for it to animate properly.
There is an easy fix to get the second one to animate immediately. It's by adding animated = false
to the second button, but then it makes the first shape do some unexpected stuff (immediately putting it to true
also doesn't work).
There is another fix for this exact example. By adding @State var animated2
and adjusting the second rectangle for animated2
would work, but I need it to work dynamically and not have to manually do it for every view - so that also goes out of the window.
You can just copy and paste it to see how it works. Any suggestions?
import SwiftUI
struct ContentView: View {
@State private var showWater: Bool = false
@State private var showWater2: Bool = false
@State private var animated = false
var body: some View {
ZStack {
if showWater {
HStack {
// First Recangle with Shape
ZStack {
Rectangle().frame(width: 200, height: 220)
Wave(offset: Angle(degrees: animated ? 360 : 0), percent: Double(50)/100)
.fill(Color(red: 0, green: 0.5, blue: 0.75, opacity: 0.5))
.frame(width: 200, height: 220)
.animation(.linear(duration: 2).repeatForever(autoreverses: false), value: animated)
.onAppear {
animated = true
}
.onDisappear {
animated = false
}
Wave(offset: Angle(degrees: animated ? -180 : 180), percent: Double(50)/100)
.fill(Color(red: 0, green: 0.5, blue: 0.75, opacity: 0.5))
.opacity(0.5)
.frame(width: 200, height: 220)
.animation(.linear(duration: 2).repeatForever(autoreverses: false), value: animated)
.onAppear {
animated = true
}
.onDisappear {
animated = false
}
}
if showWater2 {
// Second Recangle with Shape
ZStack {
Rectangle().frame(width: 200, height: 220)
Wave(offset: Angle(degrees: animated ? 360 : 0), percent: Double(50)/100)
.fill(Color(red: 0, green: 0.5, blue: 0.75, opacity: 0.5))
.frame(width: 200, height: 220)
.animation(.linear(duration: 2).repeatForever(autoreverses: false), value: animated)
.onAppear {
animated = true
}
.onDisappear {
animated = false
}
Wave(offset: Angle(degrees: animated ? -180 : 180), percent: Double(50)/100)
.fill(Color(red: 0, green: 0.5, blue: 0.75, opacity: 0.5))
.opacity(0.5)
.frame(width: 200, height: 220)
.animation(.linear(duration: 2).repeatForever(autoreverses: false), value: animated)
}
.onAppear {
animated = true
}
.onDisappear {
animated = false
}
}
}
}
HStack {
Button("Water") {
showWater.toggle()
}
if showWater {
Button("Water2") {
// animated = false
showWater2.toggle()
}
}
}
.offset(y: -150)
}
}
}
struct Wave: Shape {
var offset: Angle
var percent: Double
var animatableData: Double {
get { offset.degrees }
set { offset = Angle(degrees: newValue) }
}
func path(in rect: CGRect) -> Path {
var p = Path()
let waveHeight = 0.015 * rect.height
let yOffset = CGFloat(1 - percent) * (rect.height - 4 * waveHeight) + 2 * waveHeight
let startAngle = offset
let endAngle = offset + Angle(degrees: 360)
p.move(to: CGPoint(x: 0, y: yOffset + waveHeight * CGFloat(sin(offset.radians))))
for angle in stride(from: startAngle.degrees, through: endAngle.degrees, by: 5) {
let x = CGFloat((angle - startAngle.degrees) / 360) * rect.width
p.addLine(to: CGPoint(x: x, y: yOffset + waveHeight * CGFloat(sin(Angle(degrees: angle).radians))))
}
p.addLine(to: CGPoint(x: rect.width, y: rect.height))
p.addLine(to: CGPoint(x: 0, y: rect.height))
p.closeSubpath()
return p
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
ContentView()
}
.environment(\.colorScheme, .dark)
}
}
Swift animates by observing a before state and an after state. Your first wave animation works because animated = false
is the before and animated = true
is the after. For the second wave, animated
is always true
, so there is no change of state to animate.
As you noted, giving the second view its own animated
var fixes the animation.
The best way to do this is to make a new AnimatedWave
View
and move the animated
var
inside this view. Then, adding a second wave is as simple as calling AnimatedWave()
and it will get its own copy of the animated
state var and it will animate correctly:
struct ContentView: View {
@State private var showWater = false
@State private var showWater2 = false
var body: some View {
ZStack {
if showWater {
HStack {
// First Recangle with Shape
AnimatedWave()
if showWater2 {
AnimatedWave()
}
}
}
HStack {
Button("Water") {
showWater.toggle()
}
if showWater {
Button("Water2") {
showWater2.toggle()
}
}
}
.offset(y: -150)
}
}
}
struct AnimatedWave: View {
@State private var animated = false
var body: some View {
ZStack {
Rectangle().frame(width: 200, height: 220)
Wave(offset: Angle(degrees: animated ? 360 : 0), percent: Double(50)/100)
.fill(Color(red: 0, green: 0.5, blue: 0.75, opacity: 0.5))
.frame(width: 200, height: 220)
.animation(.linear(duration: 2).repeatForever(autoreverses: false), value: animated)
.onAppear {
animated = true
}
.onDisappear {
animated = false
}
Wave(offset: Angle(degrees: animated ? -180 : 180), percent: Double(50)/100)
.fill(Color(red: 0, green: 0.5, blue: 0.75, opacity: 0.5))
.opacity(0.5)
.frame(width: 200, height: 220)
.animation(.linear(duration: 2).repeatForever(autoreverses: false), value: animated)
}
.onAppear {
animated = true
}
.onDisappear {
animated = false
}
}
}