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

SwiftUI计时器异常:自动加30分钟至倒计时且时间随机跳转

SwiftUI计时器时间异常问题修复

问题现象

修改YouTube博主Indently的计时器教程代码,实现固定5分钟倒计时,但运行时点击Start后显示35分钟,再次点击时间随机变化。

问题原因

  1. 时区偏移导致时间计算错误:通过Date(timeIntervalSince1970: diff)转换剩余秒数为Date后,使用本地日历取分钟组件,会引入时区/夏令时的偏移,导致分钟数异常。
  2. 重复点击Start时错误累加时间:start方法中每次点击都会在当前endDate基础上添加分钟,而非从当前时间重新计算结束时间。
  3. 不必要的参数传递:目标是固定5分钟倒计时,却仍传递vm.minutes作为参数,可能引入变量干扰。

修复代码

修复后的ViewModel

import Foundation

extension TimerView {
    final class ViewModel: ObservableObject {
        @Published var isActive = false
        @Published var showingAlert = false
        @Published var time: String = "5:00"
        @Published var minutes: Float = 5.0 {
            didSet {
                self.time = "\(Int(minutes)):00"
            }
        }
        let initialTime = 5 // 固定初始时间为5分钟
        var endDate = Date()
        
        // 启动计时器,固定5分钟
        func start() {
            self.endDate = Calendar.current.date(byAdding: .minute, value: initialTime, to: Date())!
            self.isActive = true
        }
        
        // 重置计时器
        func reset() {
            self.minutes = Float(initialTime)
            self.isActive = false
            self.time = "\(Int(minutes)):00"
        }
        
        // 更新倒计时显示
        func updateCountdown(){
            guard isActive else { return }
            
            let now = Date()
            let diff = endDate.timeIntervalSince(now)
            
            if diff <= 0 {
                self.isActive = false
                self.time = "0:00"
                self.showingAlert = true
                return
            }
            
            // 直接通过秒数计算分钟和秒,避免时区问题
            let totalSeconds = Int(diff)
            let remainingMinutes = totalSeconds / 60
            let remainingSeconds = totalSeconds % 60
            
            self.minutes = Float(remainingMinutes)
            self.time = String(format:"%d:%02d", remainingMinutes, remainingSeconds)
        }
    }
}

修复后的TimerView

import SwiftUI

struct TimerView: View {
    @ObservedObject var vm = ViewModel()
    let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
    let width: Double = 250
    
    var body: some View {
        VStack {
            Text("Timer: \(vm.time)")
                .font(.system(size: 50, weight: .medium, design: .rounded))
                .alert("Timer done!", isPresented: $vm.showingAlert) {
                    Button("Continue", role: .cancel) {
                        vm.reset() // 弹窗关闭后重置计时器
                    }
                }
                .padding()
            
            HStack(spacing:50) {
                Button("Start") {
                    // 点击Start时,无论当前状态都重置并启动5分钟倒计时
                    vm.reset()
                    vm.start()
                }
                .padding()
                .background(Color(red: 184/255, green: 243/255, blue: 255/255))
                .foregroundColor(.black)
                .cornerRadius(10)
                .font(Font.system(size: UIFontMetrics.default.scaledValue(for: 16)))
                
                if vm.isActive {
                    Button("Pause") {
                        vm.isActive = false
                    }
                    .padding()
                    .foregroundColor(.black)
                    .background(.red)
                    .cornerRadius(10)
                    .font(Font.system(size: UIFontMetrics.default.scaledValue(for: 16)))
                } else {
                    Button("Resume") {
                        vm.isActive = true
                    }
                    .padding()
                    .foregroundColor(.black)
                    .background(.green)
                    .cornerRadius(10)
                    .font(Font.system(size: UIFontMetrics.default.scaledValue(for: 16)))
                }
            }
            .frame(width: width)
        }
        .onReceive(timer) { _ in
            vm.updateCountdown()
        }
    }
}

struct TimerView_Previews: PreviewProvider {
    static var previews: some View {
        TimerView()
    }
}

关键修复点

  • 直接通过总秒数计算剩余分钟和秒数,彻底规避时区/夏令时带来的时间偏移。
  • start方法固定使用初始5分钟,从当前时间重新计算结束时间,避免重复点击累加时间。
  • 优化Start按钮逻辑:点击时先重置再启动,确保每次点击都触发全新的5分钟倒计时。
  • 弹窗关闭后自动重置计时器,提升用户体验。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.12 05:42:10