Swift Activity Kit中锁屏下计时器不被终止的实现咨询
问题原因分析
你遇到的核心问题是:Timer.scheduledTimer依赖App主线程的RunLoop运行,当App锁屏进入后台后,iOS会在约30秒后挂起App的主线程RunLoop,导致计时器停止执行。而Live Activity本身有独立的生命周期,所以会继续运行,但因为没有App的状态更新,显示会停滞。
解决方案
方案一:将计时逻辑迁移至Live Activity内部(推荐)
把计时逻辑完全放到Live Activity的Extension中,让Live Activity自主处理计时和状态更新,彻底摆脱对App后台运行的依赖。
步骤1:定义Attributes和ContentState
struct LockscreenAttributes: ActivityAttributes { public struct ContentState: Codable, Hashable { var elapsedTime: Int var currentSet: Int var currentPhase: Phase // 自定义阶段枚举 } let trainingTime: Int let restTime: Int let totalSets: Int } enum Phase: Codable, Hashable { case training case rest }
步骤2:在Live Activity中实现内部计时逻辑
struct LockscreenLiveActivity: Widget { var body: some WidgetConfiguration { ActivityConfiguration(for: LockscreenAttributes.self) { context in // 锁屏界面UI VStack { Text(context.state.currentPhase == .training ? "训练中" : "休息中") Text("\(context.state.elapsedTime)") Text("第 \(context.state.currentSet)/\(context.attributes.totalSets) 组") } } dynamicIsland: { context in // Dynamic Island 界面实现(按需编写) DynamicIsland { DynamicIslandExpandedRegion(.leading) { Text("\(context.state.elapsedTime)") } } compactLeading: { Text("\(context.state.elapsedTime)") } compactTrailing: { Text(context.state.currentPhase == .training ? "训" : "休") } minimal: { Text("\(context.state.elapsedTime)") } } .onRunTask { context in // 启动Live Activity内部的计时器 let timer = Timer.publish(every: 1, on: .main, in: .common) .autoconnect() .sink { _ in var newState = context.state if newState.elapsedTime > 0 { newState.elapsedTime -= 1 Task { await context.update(newState) } } else { // 切换阶段或组数 if newState.currentPhase == .training { newState.currentPhase = .rest newState.elapsedTime = context.attributes.restTime } else { if newState.currentSet < context.attributes.totalSets { newState.currentSet += 1 newState.currentPhase = .training newState.elapsedTime = context.attributes.trainingTime } else { // 完成所有组数,结束Live Activity Task { await context.end() } } } Task { await context.update(newState) } } } await withTaskCancellationHandler { timer.cancel() } operation: { await Task.never() } } } }
步骤3:App端初始化Live Activity
在App中只需要启动Live Activity并传递初始参数即可,无需维护计时器:
func startTrainingTimer() { let attributes = LockscreenAttributes( trainingTime: 30, restTime: 15, totalSets: 3 ) let initialState = LockscreenAttributes.ContentState( elapsedTime: 30, currentSet: 1, currentPhase: .training ) do { let activity = try Activity.request( attributes: attributes, contentState: initialState, pushType: nil ) // 保存activity引用(按需) } catch { print("启动Live Activity失败:\(error.localizedDescription)") } }
方案二:使用后台任务延长App后台运行时间
如果需要保留App内的计时逻辑,可以通过后台任务申请额外的后台执行时间,但注意iOS对后台任务的时长有限制(通常几分钟),适合短周期计时场景。
步骤1:配置Info.plist
添加UIBackgroundModes键,包含processing值。
步骤2:实现后台任务管理
class TimerManager { var timer: Timer? var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid func startTimer() { timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in self?.updateTimer() self?.refreshBackgroundTask() } } private func updateTimer() { // 你的原有计时逻辑(略) // 更新Live Activity状态... } private func refreshBackgroundTask() { // 结束旧的后台任务 if backgroundTaskID != .invalid { UIApplication.shared.endBackgroundTask(backgroundTaskID) } // 启动新的后台任务 backgroundTaskID = UIApplication.shared.beginBackgroundTask(withName: "TimerProcessing") { [weak self] in UIApplication.shared.endBackgroundTask(self?.backgroundTaskID ?? .invalid) self?.backgroundTaskID = .invalid } } func stopTimer() { timer?.invalidate() endBackgroundTask() } private func endBackgroundTask() { if backgroundTaskID != .invalid { UIApplication.shared.endBackgroundTask(backgroundTaskID) backgroundTaskID = .invalid } } }
注意事项
- Live Activity的最长运行时间为8小时,完全满足常规训练计时需求。
- 确保已在项目中开启Live Activity支持:
Info.plist中NSSupportsLiveActivities设为YES,并添加对应的Widget Extension。 - 若使用方案二,后台任务的时长受iOS系统限制,无法保证长时间后台运行,仅适合短周期场景。
内容的提问来源于stack exchange,提问作者Nikita
相关产品推荐
相关产品推荐

