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

如何让Watch OS计时器在后台持续倒计时?

问题描述

我开发了一个简易的计时器应用,但发现Apple Watch的文档远不如iPhone的完善,很难找到相关问题的解决方案。

以下是我的ContentView代码,计时器的秒数存储在timeVal中:

struct ContentView: View {

@State var timeVal = 0.0
@State var timerScreenShow:Bool = false

var body: some View {
    VStack {
        HStack{
            Spacer()
            Text("\(self.timeVal.hourMinute)")
            Spacer()
        }
        
        //various buttons to set timer
        
        NavigationLink(
            destination: TimerView(timerScreenShow: self.$timerScreenShow, timeVal: Double(Int(self.timeVal)), initialTime: Int(self.timeVal)),
            isActive: $timerScreenShow,
            label: {
                Text("Start")
            }).background(Color.green).cornerRadius(22)
    }
}

以下是计时逻辑部分的代码:

struct TimerView: View {
@Binding var timerScreenShow:Bool
@State var timeVal:Double
let initialTime:Int

var body: some View {
    if timeVal > -1 {
        VStack {
            ZStack {
                Text("\(self.timeVal.hourMinuteSecond)").font(.system(size: 20))
                    .onAppear() {
                        Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
                            if self.timeVal > -1 {
                                self.timeVal -= 1
                            }
                        }
                    }
                
                //Text("\(self.timeVal.hourMinuteSecond)")
                ProgressBar(progress: Int(self.timeVal), initial: self.initialTime).frame(width: 90.0, height: 90.0)
            }
            Button(action: {
                self.timerScreenShow = false
            }, label: {
                Text("Cancel")
                    .foregroundColor(Color.red)
            })
            .padding(.top)
        }
    } else {
        Button(action: {
            self.timerScreenShow = false
        }, label: {
            Text("Done!")
                .foregroundColor(Color.green)
        }).onAppear() {
            WKInterfaceDevice.current().play(.notification)
        }
    }
}

请问我需要做哪些操作,才能让该计时器在Apple Watch应用关闭后仍能继续运行?


解决方案

Apple Watch应用在后台无法持续运行普通Timer,要实现后台持续计时,必须换思路:记录计时器的结束时间戳,而非实时倒计时,结合后台任务和本地通知来实现。

1. 替换计时逻辑:存储结束时间而非剩余秒数

  • 启动计时器时,计算并存储目标结束时间戳(当前时间 + 总秒数),用UserDefaults持久化存储。
  • 每次App启动/唤醒时,通过当前时间和结束时间的差值,计算剩余时间,不再依赖后台跑Timer。

2. 注册后台任务,定期唤醒App更新状态

Apple Watch允许注册后台刷新任务,用来在后台检查计时器状态,提前触发通知:

步骤1:配置Info.plist

在Watch Extension的Info.plist中添加:

  • 新增WKBackgroundModes数组,包含refresh-background-task
  • 新增BGTaskSchedulerPermittedIdentifiers数组,添加你的任务标识符(比如com.yourdomain.timer.refresh)

步骤2:在ExtensionDelegate中处理后台任务

import WatchKit
import BackgroundTasks
import UserNotifications

class ExtensionDelegate: NSObject, WKExtensionDelegate {
    func applicationDidFinishLaunching() {
        // 注册后台任务
        WKBackgroundTaskScheduler.shared.register(forTaskWithIdentifier: "com.yourdomain.timer.refresh", using: nil) { task in
            self.handleRefreshTask(task: task as! WKApplicationRefreshBackgroundTask)
        }
        
        // 请求通知权限
        UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { _, _ in }
    }
    
    private func handleRefreshTask(task: WKApplicationRefreshBackgroundTask) {
        let defaults = UserDefaults.standard
        guard let endTime = defaults.value(forKey: "timerEndTime") as? Double else {
            task.setTaskCompletedWithSnapshot(false)
            return
        }
        
        let now = Date().timeIntervalSince1970
        let remaining = endTime - now
        
        if remaining <= 0 {
            // 计时器结束,触发通知
            let content = UNMutableNotificationContent()
            content.title = "计时器已结束"
            content.body = "时间到了!"
            content.sound = .default
            
            let request = UNNotificationRequest(identifier: "timerFinish", content: content, trigger: nil)
            UNUserNotificationCenter.current().add(request)
            
            // 清除存储的结束时间
            defaults.removeObject(forKey: "timerEndTime")
        } else {
            // 重新注册下一次后台任务(最多提前15分钟)
            let nextTrigger = Date(timeIntervalSinceNow: min(remaining, 15*60))
            WKBackgroundTaskScheduler.shared.schedule(WKApplicationRefreshBackgroundTask(identifier: "com.yourdomain.timer.refresh") { _ in }) { error in
                if let error = error {
                    print("后台任务调度失败: \(error)")
                }
            }
        }
        
        task.setTaskCompletedWithSnapshot(false)
    }
}

3. 修改计时器启动与展示逻辑

更新ContentView的启动逻辑

// 在ContentView的Start按钮点击逻辑中(或NavigationLink跳转前)
let totalSeconds = Int(timeVal)
let endTime = Date().timeIntervalSince1970 + Double(totalSeconds)
UserDefaults.standard.set(endTime, forKey: "timerEndTime")

// 注册第一次后台任务
let triggerDate = Date(timeIntervalSinceNow: min(Double(totalSeconds), 15*60))
WKBackgroundTaskScheduler.shared.schedule(WKApplicationRefreshBackgroundTask(identifier: "com.yourdomain.timer.refresh") { _ in }) { error in
    if let error = error {
        print("调度后台任务失败: \(error)")
    }
}

重构TimerView

struct TimerView: View {
    @Binding var timerScreenShow: Bool
    @State private var remainingSeconds: Int
    private let endTime: Double
    
    init(timerScreenShow: Binding<Bool>, totalSeconds: Int) {
        self._timerScreenShow = timerScreenShow
        let endTime = Date().timeIntervalSince1970 + Double(totalSeconds)
        self.endTime = endTime
        self._remainingSeconds = State(initialValue: totalSeconds)
    }
    
    var body: some View {
        VStack {
            Text("\(remainingSeconds.hourMinuteSecond)")
                .font(.system(size: 20))
                .onAppear {
                    Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
                        let now = Date().timeIntervalSince1970
                        if now >= endTime {
                            timer.invalidate()
                            remainingSeconds = 0
                            WKInterfaceDevice.current().play(.notification)
                            timerScreenShow = false
                        } else {
                            remainingSeconds = Int(endTime - now)
                        }
                    }
                }
            
            ProgressBar(progress: remainingSeconds, initial: Int(endTime - Date().timeIntervalSince1970 + Double(remainingSeconds)))
                .frame(width: 90, height: 90)
            
            Button(action: {
                timerScreenShow = false
                // 取消后台任务与存储的结束时间
                UserDefaults.standard.removeObject(forKey: "timerEndTime")
                WKBackgroundTaskScheduler.shared.cancelAllTasks()
            }) {
                Text("Cancel")
                    .foregroundColor(.red)
            }
            .padding(.top)
        }
    }
}

关键注意点

  • Apple Watch后台任务调度有严格限制,无法保证精确唤醒,核心逻辑必须依赖结束时间戳计算剩余时间。
  • 当App完全关闭时,后台任务无法唤醒,但本地通知会在计时器结束时触发(若后台任务提前调度了通知)。
  • 绝对不要依赖后台持续运行Timer,Apple的后台机制不允许这种行为,必须采用时间戳+后台任务+本地通知的组合方案。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.17 07:00:57