如何让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
相关产品推荐
相关产品推荐

