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

ViewController释放后运行任务及多Item定时器管理技术问询

嘿,这个需求我之前也帮人处理过,核心就是要把Timer的生命周期和ViewController解绑——不然ViewController B被pop释放后,Timer也会跟着挂掉。我给你一套完整的实现方案,亲测好用:

核心思路:用全局单例管理所有Timer

我们需要一个独立的TimerManager单例类,专门负责所有Item的Timer创建、跟踪、销毁。这样不管ViewController是否被释放,Timer都能不受影响地继续运行,同时还能轻松管理多个Item的Timer状态。

具体实现步骤

1. 实现Timer管理单例

这个类会用字典存储每个Item对应的Timer和剩余时间,同时提供启动、停止、查询状态的方法,并用通知和ViewController同步UI状态:

class TimerManager {
    // 单例实例,确保全局唯一
    static let shared = TimerManager()
    private init() {}
    
    // 存储每个Item的Timer信息,假设你的Item有唯一id作为key
    private var itemTimers: [String: (timer: Timer, remainingTime: TimeInterval)] = [:]
    
    // 启动/重置Item的Timer
    func startTimer(for itemId: String, totalTime: TimeInterval) {
        // 先检查是否已有运行中的Timer,有的话先停掉
        if let existingTimer = itemTimers[itemId]?.timer {
            existingTimer.invalidate()
        }
        
        let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] timer in
            guard let self = self, var timerInfo = self.itemTimers[itemId] else {
                timer.invalidate()
                return
            }
            
            timerInfo.remainingTime -= 1.0
            if timerInfo.remainingTime <= 0 {
                // 计时结束,清理Timer并发送通知
                timer.invalidate()
                self.itemTimers.removeValue(forKey: itemId)
                NotificationCenter.default.post(name: .timerEnded, object: itemId)
            } else {
                // 更新剩余时间并发送通知
                self.itemTimers[itemId] = (timer: timer, remainingTime: timerInfo.remainingTime)
                NotificationCenter.default.post(name: .timerUpdated, object: itemId, userInfo: ["remainingTime": timerInfo.remainingTime])
            }
        }
        
        itemTimers[itemId] = (timer: timer, remainingTime: totalTime)
        timer.fire() // 立即触发一次更新,避免UI延迟
    }
    
    // 查询Item的剩余时间
    func remainingTime(for itemId: String) -> TimeInterval? {
        return itemTimers[itemId]?.remainingTime
    }
    
    // 停止Item的Timer
    func stopTimer(for itemId: String) {
        if let timer = itemTimers[itemId]?.timer {
            timer.invalidate()
            itemTimers.removeValue(forKey: itemId)
        }
    }
    
    // 检查Item是否正在计时
    func isTimerRunning(for itemId: String) -> Bool {
        return itemTimers[itemId] != nil
    }
}

// 定义通知常量,避免拼写错误
extension NSNotification.Name {
    static let timerUpdated = NSNotification.Name("TimerUpdated")
    static let timerEnded = NSNotification.Name("TimerEnded")
}

2. 在ViewController B中对接TimerManager

B作为详情页,只需要负责和用户交互,所有Timer操作都交给单例处理,同时监听通知更新UI:

class ViewControllerB: UIViewController {
    var currentItemId: String! // 从A页面传递过来的Item唯一ID
    @IBOutlet weak var remainingTimeLabel: UILabel!
    @IBOutlet weak var startBtn: UIButton!
    @IBOutlet weak var stopBtn: UIButton!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // 初始化时同步当前Item的Timer状态
        updateTimerUI()
        
        // 监听Timer状态变化的通知
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(timerDidUpdate(_:)),
            name: .timerUpdated,
            object: currentItemId
        )
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(timerDidEnd(_:)),
            name: .timerEnded,
            object: currentItemId
        )
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        // 移除通知观察者,避免野指针
        NotificationCenter.default.removeObserver(self)
        // 注意:这里不要停止Timer!我们要让它在后台继续运行
    }
    
    @IBAction func startTimerTapped(_ sender: UIButton) {
        // 假设用户设置的总时长是60秒,实际可以从输入框获取
        let totalTime: TimeInterval = 60
        TimerManager.shared.startTimer(for: currentItemId, totalTime: totalTime)
        updateTimerUI()
    }
    
    @IBAction func stopTimerTapped(_ sender: UIButton) {
        TimerManager.shared.stopTimer(for: currentItemId)
        updateTimerUI()
    }
    
    // 更新UI状态
    private func updateTimerUI() {
        let isRunning = TimerManager.shared.isTimerRunning(for: currentItemId)
        startBtn.isEnabled = !isRunning
        stopBtn.isEnabled = isRunning
        
        if isRunning {
            let remainingTime = TimerManager.shared.remainingTime(for: currentItemId)!
            remainingTimeLabel.text = String(format: "剩余时间:%.0f秒", remainingTime)
        } else {
            remainingTimeLabel.text = "未启动计时"
        }
    }
    
    @objc private func timerDidUpdate(_ notification: Notification) {
        guard let remainingTime = notification.userInfo?["remainingTime"] as? TimeInterval else { return }
        remainingTimeLabel.text = String(format: "剩余时间:%.0f秒", remainingTime)
    }
    
    @objc private func timerDidEnd(_ notification: Notification) {
        remainingTimeLabel.text = "计时结束"
        updateTimerUI()
    }
}

3. 在ViewController A中同步Timer状态

A页面需要实时显示每个Item的计时状态,所以也要监听通知刷新列表:

class ViewControllerA: UIViewController, UITableViewDataSource, UITableViewDelegate {
    var items: [YourItemModel] = [] // 你的Item数组,每个Item要有id和name属性
    @IBOutlet weak var tableView: UITableView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.dataSource = self
        tableView.delegate = self
        
        // 监听所有Timer状态变化,刷新列表
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(refreshTimerStatus),
            name: .timerUpdated,
            object: nil
        )
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(refreshTimerStatus),
            name: .timerEnded,
            object: nil
        )
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        NotificationCenter.default.removeObserver(self)
    }
    
    // MARK: - TableView 数据源方法
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return items.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "ItemCell", for: indexPath)
        let item = items[indexPath.row]
        cell.textLabel?.text = item.name
        
        // 根据Timer状态设置cell的辅助信息
        if TimerManager.shared.isTimerRunning(for: item.id) {
            cell.accessoryType = .detailDisclosureButton
            let remainingTime = TimerManager.shared.remainingTime(for: item.id)!
            cell.detailTextLabel?.text = String(format: "计时中:%.0f秒", remainingTime)
        } else {
            cell.accessoryType = .disclosureIndicator
            cell.detailTextLabel?.text = nil
        }
        return cell
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let item = items[indexPath.row]
        // 跳转到B页面并传递ItemID
        let vcB = storyboard?.instantiateViewController(withIdentifier: "ViewControllerB") as! ViewControllerB
        vcB.currentItemId = item.id
        navigationController?.pushViewController(vcB, animated: true)
    }
    
    @objc private func refreshTimerStatus() {
        tableView.reloadData()
    }
}

4. 处理重复选中运行中Timer的Item

当用户从A页面再次选中正在计时的Item进入B时,B的viewDidLoad会自动调用updateTimerUI(),显示当前剩余时间并禁用启动按钮——用户可以看到实时计时状态,也可以选择停止Timer。

关键注意事项
  • 内存泄漏防护:Timer闭包中一定要用[weak self]避免循环引用,Timer结束后要从字典中移除对应条目,防止内存泄漏。
  • 后台运行限制:如果需要Timer在APP后台继续运行,默认方案不行,iOS有严格的后台限制,你需要申请对应后台模式(比如音频、定位),或者用BackgroundTasks框架实现,这个可以根据需求再扩展。

内容的提问来源于stack exchange,提问作者Tejas K

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.25 03:34:52