A Timer with target: self keeps a view controller alive forever — fix it
This view controller starts a repeating Timer to refresh a label. Its deinit is never called: after the user pops the screen the controller stays in memory forever.
Constraints:
- The timer must keep firing while the screen is visible.
- After pop, the controller must deallocate (its
deinitmust run).
final class ClockViewController: UIViewController {
var timer: Timer?
override func viewDidLoad() {
super.viewDidLoad()
timer = Timer.scheduledTimer(
timeInterval: 1, target: self,
selector: #selector(tick), userInfo: nil, repeats: true)
}
@objc func tick() { /* update label */ }
}
Find and fix the bug.
A scheduled Timer retains its target, and the run loop retains the timer, so target: self keeps the controller alive forever. Fix it with invalidate() in viewDidDisappear, or the block API with [weak self].
- ✗Marking the
Timerpropertyweak— it does not break the run loop's hold - ✗Believing
Timerdoes not retain its target - ✗Expecting ARC or memory pressure to release the controller anyway
- →Why does an invalidated timer finally release its target?
- →How does the block-based
TimerAPI let you avoid the strongtarget?
The ownership chain is: run loop -> Timer -> target (the controller). Marking the timer property weak does nothing — it is the run loop, not the property, that holds the timer. Either invalidate the timer when the screen leaves, or move to the block API, which needs no strong target:
override func viewDidLoad() {
super.viewDidLoad()
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
self?.tick()
}
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
timer?.invalidate() // the run loop releases the timer, chain breaks
}
invalidate() removes the timer from the run loop, which releases it, and the last strong reference to the controller disappears — deinit runs.