SwiftUI中如何为单个元素同时执行多个动画?
问题
我有一个WaveView,仅包含正弦波和矩形。为了模拟真实波浪效果,需要让它持续平移。于是我编写了Wave视图,在.onAppear中通过定时器为offset添加动画(也尝试过.animation(.linear(duration: 3).repeatForever(autoreverses: false), value: offset),但问题相同)。Wave视图出现后,波浪持续移动,一切正常。
之后我想为progress变量添加动画,实现进度变化的效果。期望波浪持续移动的同时,进度(可见蓝色部分的高度)能带动画上升,但进度变化时,进度动画执行的同时波浪动画会停止,直到下一次定时器触发。
使用.animation(.linear(duration: 3).repeatForever(autoreverses: false), value: offset)的情况更糟:因为没有定时器,只修改一次offset,动画被中断后波浪就不再移动了。
请问如何在执行progress动画的同时保持波浪动画持续运行?
附上代码:
struct WaveView: View { var waveHeight: CGFloat var body: some View { GeometryReader { global in Path { path in let width = global.size.width path.move(to: CGPoint(x: width*2.0, y: waveHeight)) path.addLine(to: CGPoint(x:width*2.0,y: global.size.height)) path.addLine(to: CGPoint(x:0,y:global.size.height)) path.addLine(to: CGPoint(x:0,y: waveHeight)) var points = [CGPoint]() for angle in stride(from: 0, through: 180.0*4, by: 1) { let radian = angle * .pi / 180 let cosValue = cos(radian) let x = CGFloat(angle) * global.size.width / 360 let y = (1+cosValue) * waveHeight / 2 points.append(CGPoint(x: x, y: y)) } path.addLines(points) } .fill(.blue.opacity(0.5)) } } } struct Wave: View { init(progress: Binding<Float>, waveHeight height: CGFloat = 200) { waveHeight = height self._progress = progress } var waveHeight: CGFloat @Binding var progress: Float @State var offset: CGFloat = 0 @State var timer: Timer? var body: some View { GeometryReader { global in ZStack(alignment: .bottom) { WaveView(waveHeight: waveHeight) .onAppear { timer = .scheduledTimer(withTimeInterval: 3, repeats: true, block: { _ in if offset >= 1 { offset = 0 } withAnimation(.linear(duration: 3)) { offset += 1 } }) timer?.fire() } .onDisappear { timer?.invalidate() timer = nil } } .frame(width: global.size.width, height: global.size.height) .position(x: global.size.width * (0.5-offset),y: global.size.height/2) .offset(y:CGFloat(1-progress)*global.size.height) .animation(.easeInOut(duration: 0.4), value: progress) } } } struct TestView: View { @State var progress: Float = 0.6 var body: some View { ZStack{ Wave(progress: $progress, waveHeight: 35) .edgesIgnoringSafeArea(.all) VStack { Stepper("progress: \(progress.description)",value: $progress, in: 0.0...1.0, step: 0.2) .padding(.horizontal,40) } } } } #Preview { TestView() }
效果对比:
- 正常波浪效果:

- 被进度动画中断的波浪效果:

解决方案
问题根源在于两个动画共享了同一个视图的动画上下文,导致进度动画触发时会打断波浪的平移动画。要解决这个问题,需要将两个动画的作用范围隔离开,让它们各自独立运行。
修改步骤
- 分离动画作用域:把波浪平移逻辑封装到单独的子视图中,只为该子视图绑定平移动画,避免和进度动画的上下文冲突。
- 替换定时器为永久动画:使用
.repeatForever的动画直接驱动波浪平移,无需定时器,动画更稳定且不易被打断。 - 明确动画触发条件:为进度动画指定仅响应
progress变量变化,确保两个动画互不干扰。
修改后的完整代码
struct WaveView: View { var waveHeight: CGFloat var body: some View { GeometryReader { global in Path { path in let width = global.size.width path.move(to: CGPoint(x: width*2.0, y: waveHeight)) path.addLine(to: CGPoint(x:width*2.0,y: global.size.height)) path.addLine(to: CGPoint(x:0,y:global.size.height)) path.addLine(to: CGPoint(x:0,y: waveHeight)) var points = [CGPoint]() for angle in stride(from: 0, through: 180.0*4, by: 1) { let radian = angle * .pi / 180 let cosValue = cos(radian) let x = CGFloat(angle) * global.size.width / 360 let y = (1+cosValue) * waveHeight / 2 points.append(CGPoint(x: x, y: y)) } path.addLines(points) } .fill(.blue.opacity(0.5)) } } } // 封装波浪平移的独立子视图 struct AnimatedWaveView: View { let waveHeight: CGFloat @State private var offset: CGFloat = 0 var body: some View { WaveView(waveHeight: waveHeight) .frame(maxWidth: .infinity, maxHeight: .infinity) .position(x: UIScreen.main.bounds.width * (0.5 - offset), y: UIScreen.main.bounds.height / 2) .onAppear { // 启动永久循环的平移动画 withAnimation(.linear(duration: 3).repeatForever(autoreverses: false)) { offset = 1 } } } } struct Wave: View { init(progress: Binding<Float>, waveHeight height: CGFloat = 200) { waveHeight = height self._progress = progress } var waveHeight: CGFloat @Binding var progress: Float var body: some View { GeometryReader { global in ZStack(alignment: .bottom) { AnimatedWaveView(waveHeight: waveHeight) } .frame(width: global.size.width, height: global.size.height) .offset(y: CGFloat(1 - progress) * global.size.height) // 仅当progress变化时触发该动画 .animation(.easeInOut(duration: 0.4), value: progress) } } } struct TestView: View { @State var progress: Float = 0.6 var body: some View { ZStack{ Wave(progress: $progress, waveHeight: 35) .edgesIgnoringSafeArea(.all) VStack { Stepper("progress: \(String(format: "%.1f", progress))", value: $progress, in: 0.0...1.0, step: 0.2) .padding(.horizontal,40) .background(.white) } } } } #Preview { TestView() }
关键说明
- 波浪平移动画被封装在
AnimatedWaveView中,独立运行,不受外部进度动画的影响。 - 使用
.repeatForever替代定时器,动画逻辑更简洁,且不会因外部状态变化中断。 - 进度动画明确绑定
progress变量,仅在进度变化时触发,与波浪动画完全隔离。
内容的提问来源于stack exchange,提问作者richardjorne
相关产品推荐
相关产品推荐

