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

Swift开发中如何实现App锁屏状态下Timer定时器持续运行

iOS 锁屏后台 Timer 持续计时实现方案

问题根源

你当前的实现存在两个核心问题:

  1. 依赖Timer逐秒累加时间,Timer本身受Runloop调度影响,主线程卡顿、页面滚动、App进入后台都会导致Timer触发暂停,时间累计会出现偏差
  2. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.08 04:48:03