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

SwiftUI中ViewModel的@Published属性初始变更无法触发动画求解

问题分析

首次从isLoading状态切换到显示CircularProgressView时,进度动画未触发,但后续progress变更能正常触发动画,核心原因是:

  • 首次创建CircularProgressView时,视图是全新初始化渲染的,此时progress从ViewModel的初始值0直接跳转为Timer设置的随机值,SwiftUI默认不会对视图的初始渲染状态应用动画(无前置状态可过渡)。
  • 后续progress变更都是在视图已存在的前提下发生的,状态变化的前后对比清晰,动画能正常触发。

解决方案

方案一:修正动画触发的依赖值(最简洁)

修改GaugeProgressStyle,将动画的触发值从外部传入的progress改为ProgressView自身的fractionCompleted,这样首次渲染时,进度从0过渡到目标值的变化会被动画捕捉:

struct GaugeProgressStyle: ProgressViewStyle {
    func makeBody(configuration: Configuration) -> some View {
        let fractionCompleted = configuration.fractionCompleted ?? 0

        return ZStack {
            Circle()
                .trim(from: 0, to: fractionCompleted)
                .stroke(Color.blue, style: StrokeStyle(lineWidth: 10, lineCap: .round))
                .rotationEffect(.degrees(-90))
                .animation(.easeOut.speed(0.35), value: fractionCompleted)
        }
    }
}

同时简化CircularProgressView中样式的调用:

ProgressView(value: progress, total: 1.0)
    .progressViewStyle(GaugeProgressStyle()) // 移除多余的progress参数

方案二:显式包裹状态变更的动画

在ViewModel的Timer闭包里,用withAnimation显式包裹状态变更,强制SwiftUI为首次状态切换应用动画:

@MainActor
class ViewModel: ObservableObject {
    @Published var progress: Double = 0.00
    @Published var isLoading: Bool = true
    
    init() {
        Timer.scheduledTimer(withTimeInterval: 1.5, repeats: true) { timer in
            withAnimation(.easeOut.speed(0.35)) {
                self.progress = Double.random(in: 0...1)
                self.isLoading = false
            }
        }
    }
}

方案三:手动触发初始动画过渡

在CircularProgressView中添加一个临时状态变量,通过onAppear手动触发从0到目标进度的动画过渡:

struct CircularProgressView: View {
    let progress: Double
    @State private var displayedProgress: Double = 0.0
    
    var body: some View {
        ZStack {
            Circle()
                .stroke(Color.gray.opacity(0.2), style: StrokeStyle(lineWidth: 10))
            ProgressView(value: displayedProgress, total: 1.0)
                .progressViewStyle(GaugeProgressStyle(progress: displayedProgress))
                .contentShape(Rectangle())
            
            Text("\(Int(displayedProgress*100.rounded()))")
            
        }
        .padding()
        .onAppear {
            withAnimation(.easeOut.speed(0.35)) {
                displayedProgress = progress
            }
        }
        .onChange(of: progress) { newValue in
            withAnimation(.easeOut.speed(0.35)) {
                displayedProgress = newValue
            }
        }
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 08:08:12