SwiftUI中Timer与Double类型的if判断问题:兼顾进度条流畅性与触发准确性
倒计时Timer的条件触发与进度条流畅性解决方案
问题描述
我在App里用每0.1秒触发一次的Timer实现流畅的进度条动画,同时加了两个条件:倒计时到10秒时播放音效,到0秒时停止Timer。但现在条件完全触发不了;如果把countdownTimer转成Int类型,条件会连续触发10次;换成每秒触发的Int Timer,进度条又会卡顿。怎么在保持进度条流畅的同时解决这个问题?
附带原代码:
struct TimerComponent: View { @State var progress: Double = 0 @Binding var countdownTimer: Double @Binding var timerRunning: Bool let timer = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect() var body: some View { ZStack { CircularProgressComponent(progress: progress, timerRunning: $timerRunning) .frame(width: 120, height: 120) Text("\(Int(countdownTimer))") .onReceive(timer){ _ in if countdownTimer > 0 && timerRunning { countdownTimer -= 0.1 progress = 1 - Double(countdownTimer)/60 if countdownTimer == 10.0 { SoundManager.instance.playSound(sound: .flatline) } } else if countdownTimer == 0.0 { timerRunning = false countdownTimer = -1 } } .font(Font.custom("AnnieUseYourTelescope-Regular", size: 40)) .frame(minWidth: 80, maxHeight: 50) .padding(10) .foregroundColor(.black) .cornerRadius(10) .shadow(radius: 50) } } }
核心问题分析
- 浮点数精度误差:直接用
countdownTimer == 10.0或countdownTimer == 0.0判断会失效——每次减0.1的操作多次累积后,数值会变成类似9.999999999999998或0.000000000000001的近似值,永远不等于精确整数。 - 重复触发逻辑:转成Int后,0.1秒触发一次的Timer会在10秒到9.1秒之间连续10次让
Int(countdownTimer)等于10,导致音效重复播放。
解决方案
方案1:阈值范围判断(简单直接)
放弃精确等于的判断,改用数值落在极小范围内来判定是否到达目标时间,同时添加标记避免重复触发:
struct TimerComponent: View { @State var progress: Double = 0 @Binding var countdownTimer: Double @Binding var timerRunning: Bool @State private var hasPlayedTenSecondSound = false // 新增:标记音效是否已播放 let timer = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect() var body: some View { ZStack { CircularProgressComponent(progress: progress, timerRunning: $timerRunning) .frame(width: 120, height: 120) Text("\(Int(countdownTimer))") .onReceive(timer){ _ in if countdownTimer > 0 && timerRunning { countdownTimer -= 0.1 progress = 1 - countdownTimer/60 // 用阈值范围判断是否接近10秒,且仅播放一次 if countdownTimer < 10.1 && countdownTimer > 9.9 && !hasPlayedTenSecondSound { SoundManager.instance.playSound(sound: .flatline) hasPlayedTenSecondSound = true } } else if countdownTimer < 0.1 { // 用阈值判断是否接近0秒 timerRunning = false countdownTimer = -1 } } .font(Font.custom("AnnieUseYourTelescope-Regular", size: 40)) .frame(minWidth: 80, maxHeight: 50) .padding(10) .foregroundColor(.black) .cornerRadius(10) .shadow(radius: 50) } } }
方案2:跟踪整秒数变化(精准控制单次触发)
添加状态变量记录上一次的整秒数值,仅当整秒数发生变化且等于目标值时触发逻辑:
struct TimerComponent: View { @State var progress: Double = 0 @Binding var countdownTimer: Double @Binding var timerRunning: Bool @State private var lastDisplayedSecond: Int = -1 // 新增:记录上一次的整秒数 let timer = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect() var body: some View { ZStack { CircularProgressComponent(progress: progress, timerRunning: $timerRunning) .frame(width: 120, height: 120) Text("\(Int(countdownTimer))") .onReceive(timer){ _ in guard countdownTimer > 0 && timerRunning else { if countdownTimer <= 0.1 { timerRunning = false countdownTimer = -1 } return } countdownTimer -= 0.1 progress = 1 - countdownTimer/60 let currentSecond = Int(countdownTimer.rounded()) // 取当前整秒数 if currentSecond != lastDisplayedSecond { lastDisplayedSecond = currentSecond // 仅当整秒数变化到10时触发音效 if currentSecond == 10 { SoundManager.instance.playSound(sound: .flatline) } } } .font(Font.custom("AnnieUseYourTelescope-Regular", size: 40)) .frame(minWidth: 80, maxHeight: 50) .padding(10) .foregroundColor(.black) .cornerRadius(10) .shadow(radius: 50) } } }
方案优势
- 两种方案都保留了0.1秒间隔的Timer,确保进度条动画流畅;
- 方案1实现简单,适合快速解决问题;
- 方案2通过跟踪整秒数变化,完全避免重复触发,逻辑更精准。
内容的提问来源于stack exchange,提问作者TheDoctor
相关产品推荐
相关产品推荐

