SwiftUI中如何通过按钮实现Timer计时器的暂停功能
SwiftUI Timer 暂停功能实现方案
你当前代码无法暂停的核心原因有两个:一是定义的isPaused状态变量没有接入任何计时控制逻辑,点击按钮仅修改变量值不会影响Timer运行;二是使用autoconnect()创建的自动连接Timer没有保留控制句柄,无法主动触发停止/恢复。
核心修改思路
- 移除Timer创建链式调用里的
autoconnect(),新增Cancellable类型的状态变量持有Timer连接句柄,用来手动控制Timer的启停 - 视图首次加载时主动调用
connect()启动Timer,把返回的连接实例存到状态变量中 - 暂停按钮点击时切换
isPaused状态:暂停时调用连接实例的cancel()方法终止Timer事件推送,恢复时重新调用connect()启动Timer - 按钮图标根据
isPaused状态动态切换为暂停/继续样式 - 计时回调中增加状态判断做兜底,避免异常场景下计时错乱
可直接运行的修改后代码
注意需要先导入Combine框架,Cancellable是该框架提供的类型:
import SwiftUI import Combine struct TestView: View { @State var isTimeStarted = false @State var to: CGFloat = 0 @State var timeDuration = 60 // 移除autoconnect,仅保留Timer发布者实例 @State var time = Timer.publish(every: 1, on: .main, in: .common) @State var isPaused = false @State private var count = 0 // 持有Timer连接句柄,用于控制暂停/恢复 @State var timerCancellable: Cancellable? var body: some View { ZStack { VStack { ZStack { Circle() .trim(from: 0, to: self.to) .stroke(LinearGradient(gradient: Gradient(colors: [Color.white, Color.white.opacity(0.2)]), startPoint: .topLeading, endPoint: .bottomTrailing), style: StrokeStyle(lineWidth: 15.6, lineCap: .round)) .shadow(radius: 8) .rotationEffect(.degrees(90)) .rotation3DEffect(Angle(degrees: 180), axis: (x: 1, y: 0, z: 110)) .frame(width: 70, height: 70) .animation(.easeOut) .padding() .padding(.leading, 10) Text("\(self.timeDuration, specifier: formatTime())") .font(.system(size: 17.5, design: .rounded)) .fontWeight(.semibold) .foregroundColor(.white) .padding(.leading, 10) } Button { isPaused.toggle() if isPaused { // 暂停:断开Timer连接 timerCancellable?.cancel() } else { // 恢复:重新建立Timer连接 timerCancellable = time.connect() } } label: { ZStack { BlurView2(style: .systemThinMaterialDark) .frame(width: 145, height: 45) .background(Color.yellow) .cornerRadius(30) .padding(.horizontal) // 动态切换按钮图标 Image(systemName: isPaused ? "play.fill" : "pause") .font(.title2) .shadow(radius: 10) .foregroundColor(.yellow) } } } } .preferredColorScheme(.dark) .onAppear { self.timeDuration = 60 withAnimation(.default) { // 修复原代码进度值超出0-1范围的问题,trim的to参数取值范围为0~1 self.to = 1 } self.isTimeStarted = true // 首次进入页面主动启动Timer self.timerCancellable = self.time.connect() } .onReceive(self.time, perform: { _ in // 兜底判断:暂停状态下不执行计时逻辑 guard !isPaused else { return } if self.timeDuration > 0 { self.timeDuration -= 1 withAnimation(.default) { self.to = CGFloat(self.timeDuration)/60 } } else { // 计时结束重置状态,自动暂停 self.timeDuration = 60 self.to = 1 timerCancellable?.cancel() isPaused = true } }) } func formatTime() -> String { let minutes = Int(timeDuration) / 60 % 60 let seconds = Int(timeDuration) % 60 return String(format: "%02i:%02i", minutes,seconds) } } struct BlurView2: UIViewRepresentable { var style: UIBlurEffect.Style func makeUIView(context: Context) -> UIVisualEffectView { let view = UIVisualEffectView(effect: UIBlurEffect(style: style)) return view } func updateUIView(_ uiView: UIVisualEffectView, context: Context) { } }
顺带修复的原有逻辑问题
- 原代码圆形进度条初始值设为60,超出了
trim修饰符要求的0~1取值范围,导致进度条初始状态直接填满,修正后进度值按「剩余时长/总时长」计算,符合取值要求 - 原代码计时到0后直接重置时长不会停止,修正后计时结束自动暂停,点击按钮可重新开始计时
内容的提问来源于stack exchange,提问作者user17136166
相关产品推荐
相关产品推荐

