Swift开发中如何实现App锁屏状态下Timer定时器持续运行
iOS 锁屏后台 Timer 持续计时实现方案
问题根源
你当前的实现存在两个核心问题:
- 依赖Timer逐秒累加时间,Timer本身受Runloop调度影响,主线程卡顿、页面滚动、App进入后台都会导致Timer触发暂停,时间累计会出现偏差
- App进入后台后系统默认会在30秒左右将App挂起,所有线程执行都会暂停,Timer自然无法继续触发
实现步骤
1. 重构计时逻辑(必做,不需要后台权限也能保证计时准确)
不要用Timer累加时间,改为记录开始时间戳,每次Timer触发时用当前时间和开始时间戳做差计算已运行时长,即使Timer中间暂停,恢复后计算的时间依然准确。
2. 后台保活配置(可选,需要Timer在后台实时触发才需要)
如果需要App在后台时Timer依然能实时触发(比如实时更新计时展示、到点触发本地通知),需要做后台保活配置:
- 打开Xcode项目的
Signing & Capabilities标签,点击+ Capability添加Background Modes - 根据你的App实际功能勾选对应权限:
- 普通短时间后台运行(最多3分钟):不需要额外勾选,调用系统后台任务API即可
- 运动健身类App:勾选
Activity - 音频类App:勾选
Audio, AirPlay, and Picture in Picture,可通过循环播放无声音频实现长期后台保活
注意:无合理使用场景的后台保活会被App Store审核拒绝
完整实现代码
import UIKit class TimerViewController: UIViewController { var timer: Timer? // 记录计时开始时间 var startTime: Date? // 记录暂停时已经累计的时长 var elapsedWhenPaused: TimeInterval = 0 // 后台任务ID var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid @IBOutlet weak var timerLabel: UILabel? override func viewDidLoad() { super.viewDidLoad() // 监听App进入前台/后台通知 NotificationCenter.default.addObserver(self, selector: #selector(appDidEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(appWillEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil) } func startTimer() { // 停止旧的Timer timer?.invalidate() // 记录开始时间 startTime = Date() // 初始化Timer,添加到Common模式避免滚动时暂停 timer = Timer(timeInterval: 1.0, target: self, selector: #selector(updateTimer), userInfo: nil, repeats: true) RunLoop.current.add(timer!, forMode: .common) } func pauseTimer() { timer?.invalidate() timer = nil // 保存当前已经累计的时长 if let start = startTime { elapsedWhenPaused += Date().timeIntervalSince(start) } // 结束后台任务 if backgroundTaskID != .invalid { UIApplication.shared.endBackgroundTask(backgroundTaskID) backgroundTaskID = .invalid } } @objc func updateTimer() { guard let start = startTime else { return } // 计算总时长 = 暂停累计时长 + 当前时间-开始时间 let totalSeconds = elapsedWhenPaused + Date().timeIntervalSince(start) let min = Int(totalSeconds) / 60 let sec = Int(totalSeconds) % 60 // 格式化展示,补0避免出现1:5这种异常显示 timerLabel?.text = String(format: "%d:%02d", min, sec) } @objc func appDidEnterBackground() { // 申请后台任务,延长后台运行时间 backgroundTaskID = UIApplication.shared.beginBackgroundTask(withName: "TimerBackgroundTask") { [weak self] in // 后台时间到期时执行 guard let self = self else { return } if self.backgroundTaskID != .invalid { UIApplication.shared.endBackgroundTask(self.backgroundTaskID) self.backgroundTaskID = .invalid } } } @objc func appWillEnterForeground() { // 回到前台后刷新计时展示 updateTimer() // 结束后台任务 if backgroundTaskID != .invalid { UIApplication.shared.endBackgroundTask(backgroundTaskID) backgroundTaskID = .invalid } } }
补充说明
- 如果你只需要计时准确,不需要后台实时触发Timer,只需要实现时间戳逻辑即可,不需要任何后台权限,App回到前台后会自动计算出正确的时间
- 如果需要后台到点触发提醒,可以搭配本地通知实现,不需要长期保活Timer,提前计算好触发时间注册本地通知即可,是更推荐的实现方式
内容的提问来源于stack exchange,提问作者user10469417
相关产品推荐
相关产品推荐

