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

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,内部管理自己的动画状态,避免多个视图共享同一状态导致的动画触发异常,同时支持动态添加任意数量的视图。

修改步骤:

  1. 创建独立的AnimatedWaveView,内部维护自己的动画状态变量
  2. 在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.11 00:01:16