如何提升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
相关产品推荐
相关产品推荐

