You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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()
}

效果对比:

  • 正常波浪效果:
    正常波浪效果
  • 被进度动画中断的波浪效果:
    被进度动画中断的波浪效果
解决方案

问题根源在于两个动画共享了同一个视图的动画上下文,导致进度动画触发时会打断波浪的平移动画。要解决这个问题,需要将两个动画的作用范围隔离开,让它们各自独立运行。

修改步骤

  1. 分离动画作用域:把波浪平移逻辑封装到单独的子视图中,只为该子视图绑定平移动画,避免和进度动画的上下文冲突。
  2. 替换定时器为永久动画:使用.repeatForever的动画直接驱动波浪平移,无需定时器,动画更稳定且不易被打断。
  3. 明确动画触发条件:为进度动画指定仅响应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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 17:06:06