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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 16:05:57