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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 13:39:19