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

SwiftUI视图重绘中断动画:如何避免CirclesView被重绘?

SwiftUI 独立动画视图解决方案

问题代码

struct ContentView: View {
    
    let timer = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect()
    
    @State private var num = 0
    var body: some View {
        VStack {
            Text("\(num)")
                .onReceive(timer) { _ in
                    num += 1 // 注释此行后CirclesView动画可正常运行
                }
            CirclesView()
        }
    }
}

#Preview {
    ContentView()
}

struct CirclesView: View {
    let timer = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect()
    @State private var colorOn = false
    
    var body: some View {
        ForEach(0...5, id: \.self) { n in
            CircleView(size: 30, isOn: n % 2 == 0 ? colorOn : !colorOn)
            
                .onReceive(timer) { _ in
                    colorOn.toggle()
                }
        }
    }
}

struct CircleView: View {
    var size: CGFloat
    var isOn: Bool

    var body: some View {
        Circle()
            .fill(isOn ? .red : .blue)
            .frame(width: size, height: size)
            .animation(.easeInOut(duration: 0.5), value: isOn)
    }
}

问题根源

ContentView中num是@State变量,每次计时器触发更新num时,ContentView会重绘,导致CirclesView被重新实例化,创建新的Timer Publisher,旧计时器被销毁,从而中断动画。

解决方案

方案一:用@StateObject托管动画状态

将动画的计时器和状态逻辑迁移到独立的ObservableObject类中,通过@StateObject持有,确保状态和计时器在视图重绘时不被销毁:

// 1. 创建状态管理类
class CircleAnimationManager: ObservableObject {
    let timer = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect()
    @Published var colorOn = false
    
    init() {
        // 订阅计时器事件
        _ = timer.sink { [weak self] _ in
            self?.colorOn.toggle()
        }
    }
}

// 2. 修改CirclesView
struct CirclesView: View {
    @StateObject private var manager = CircleAnimationManager()
    
    var body: some View {
        ForEach(0...5, id: \.self) { n in
            CircleView(size: 30, isOn: n % 2 == 0 ? manager.colorOn : !manager.colorOn)
        }
    }
}

方案二:让CirclesView遵循Equatable

通过实现Equatable协议,告诉SwiftUI只有当视图的依赖参数变化时才重绘:

// 修改CirclesView,添加Equatable实现
struct CirclesView: View, Equatable {
    static func == (lhs: CirclesView, rhs: CirclesView) -> Bool {
        // 视图无外部可变参数,始终视为相同实例
        return true
    }
    
    let timer = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect()
    @State private var colorOn = false
    
    var body: some View {
        ForEach(0...5, id: \.self) { n in
            CircleView(size: 30, isOn: n % 2 == 0 ? colorOn : !colorOn)
                .onReceive(timer) { _ in
                    colorOn.toggle()
                }
        }
    }
}

// 在ContentView中使用时添加.equatable()修饰
CirclesView().equatable()

方案三:固定视图ID

给CirclesView设置固定ID,让SwiftUI始终认为这是同一个视图实例:

// 在ContentView中修改CirclesView的调用
CirclesView().id("fixed-circle-animation-view")

总结

以上三种方法都能让CirclesView不受父视图状态变化影响,保持动画持续运行。其中方案一符合SwiftUI状态管理的最佳实践,逻辑清晰且易于维护,是推荐的解决方案。

内容的提问来源于stack exchange,提问作者soleil

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.28 13:43:12