SwiftUI:基于onAppear动态实现多视图动画
SwiftUI多自定义Shape动画启动问题解决方案
问题
给多个自定义Shape添加动画时,后续视图首次出现无法立即启动动画:
- 第一个Shape显示时动画正常
- 第二个Shape首次显示无动画,需移除再重新开启才生效
- 尝试共享
animated状态变量会导致已有视图动画异常,手动给每个视图加独立状态变量无法支持动态场景
复现代码:
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) } }
解决方案
核心思路是将每个带动画的Shape容器封装成独立View,内部管理自己的动画状态,避免多个视图共享同一状态导致的动画触发异常,同时支持动态添加任意数量的视图。
修改步骤:
- 创建独立的
AnimatedWaveView,内部维护自己的动画状态变量 - 在
ContentView中用这个新View替换原来的两个ZStack代码块
修改后的完整代码:
import SwiftUI struct ContentView: View { @State private var showWater: Bool = false @State private var showWater2: Bool = false var body: some View { ZStack { if showWater { HStack { // 使用独立封装的动画View AnimatedWaveView() if showWater2 { // 动态添加的视图也能正常启动动画 AnimatedWaveView() } } } HStack { Button("Water") { showWater.toggle() } if showWater { Button("Water2") { showWater2.toggle() } } } .offset(y: -150) } } } // 封装独立的带动画的Wave视图 struct AnimatedWaveView: 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) 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 } } } 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) } }
原理说明
- 每个
AnimatedWaveView实例都有自己的animated状态变量,视图出现时onAppear会将其从false设为true,触发动画启动 - 避免了多个视图共享同一状态时,新视图出现状态无变化导致动画不触发的问题
- 封装后可以动态添加任意数量的
AnimatedWaveView,每个都能独立正常启动动画
内容的提问来源于stack exchange,提问作者b.joe
相关产品推荐
相关产品推荐

