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

如何提升SwiftUI中计时器的精度,解决环形进度条归零不同步问题

问题核心原因

  • 浮点数累加/累减的精度误差:每次计算1/(Double(customInverval)*1000)做减量,经过上千次计算后会出现浮点累积误差,最终进度值无法刚好降到0
  • 终止判断逻辑错误:使用counterCircle == Double(customInverval)作为终止条件,浮点型的相等判断本身极易受精度影响触发失败,导致最后一次减量未执行或者提前终止计时
  • 双定时器逻辑冗余:两个独立定时器分开跑,很容易出现文字计时和进度条更新不同步的问题
  • 动画和数值更新不同步:进度条的线性动画没有和进度值绑定,计时结束时可能动画还未跑完

优化方案

1. 替换双定时器为单一定时器,用时间差计算进度

放弃每次加减固定值的进度计算方式,直接用「已跑时长/总计时长」计算实时进度,从根源避免浮点累积误差

2. 终止时强制重置进度为0,兜底避免进度残留

不管中间计算精度如何,计时结束后直接将progress赋值为0,确保进度条完全归零

3. 优化终止判断逻辑,用大于等于替代等于,避免浮点相等判断失效

4. 给动画绑定进度值,避免全局动画干扰导致的更新不同步


修改后的完整代码

TimerView 代码

struct TimerView: View {
    // 统一用单一定时器,也可根据需求调到0.01s,性能更好且人眼无感知
    let timer = Timer.publish(every: 0.001, on: .main, in: .common).autoconnect()
    
    @State var progress : Double = 1.0
    @State var elapsedTime: Double = 0.0 // 已跑时长,单位:秒
    @State var timerSeconds : Int = 0
    let customInterval : Int // 总计时长,单位:秒
    
    var body: some View {
        ZStack{
            ProgressCircleView(progress: $progress, timerSeconds: $timerSeconds, customInterval: customInterval)
                .padding()
            
            VStack{
                Text("\(timerSeconds)")
                    .font(.system(size: 80))
                    .bold()
                    .multilineTextAlignment(.center)
            }
            .accessibilityElement(children: .combine)
        }
        .padding()
        .onAppear {
            // 初始化剩余时间
            timerSeconds = customInterval
        }
        .onReceive(timer){ time in
            // 计时结束逻辑
            guard elapsedTime < Double(customInterval) else {
                // 强制归零,兜底避免进度残留
                progress = 0
                timerSeconds = 0
                timer.upstream.connect().cancel()
                return
            }
            // 累加已跑时间
            elapsedTime += 0.001
            // 直接用比例计算进度,无累积误差
            progress = 1 - (elapsedTime / Double(customInterval))
            // 更新剩余秒数,避免双定时器不同步
            let remaining = customInterval - Int(elapsedTime)
            if remaining != timerSeconds {
                timerSeconds = remaining
            }
        }
    }
}

ProgressCircleView 代码

struct ProgressCircleView: View {
    @Binding var progress : Double
    @Binding var timerSeconds : Int
    let customInterval : Int
    
    var body: some View {
        ZStack{
            Circle()
                .stroke(lineWidth: 25)
                .opacity(0.08)
                .foregroundColor(.black)
            
            Circle()
                .trim(from: 0.0, to: CGFloat(min(progress, 1.0)))
                .stroke(style: StrokeStyle(lineWidth: 20.0, lineCap: .round, lineJoin: .round))
                .rotationEffect(.degrees(270.0))
                .foregroundColor(getCircleColor(timerSeconds: timerSeconds))
                // 动画绑定到progress,只在progress变化时触发线性动画
                .animation(.linear(duration: 0.001), value: progress)
        }
    }
}

func getCircleColor(timerSeconds: Int) -> Color {
    if (timerSeconds <= 10 && timerSeconds > 3) {
        return Color.yellow
    } else if (timerSeconds <= 3){
        return Color.red
    } else {
        return Color.green
    }
}

额外性能优化建议

  • 如果不需要毫秒级的进度顺滑度,可以把定时器的触发间隔从0.001s改成0.01s,大幅降低CPU占用,人眼几乎感知不到区别
  • 可以改用DispatchSourceTimer替代Timer.publish,计时精度更高,且不会受ScrollView滑动等主线程耗时任务干扰

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.06 05:48:02