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
相关产品推荐
相关产品推荐

