SwiftMidOpen-ended

How do Timers and CADisplayLink cause memory leaks and how do you resolve them?

Explanation & Code

Answer: Timer and CADisplayLink cause memory leaks because the target-action API causes the active RunLoop to retain the timer, and the timer retains its target object until explicitly invalidated.

Even if you dismiss a UIViewController or release a ViewModel, if an active Timer is targeting self, its deinit will never be called because the retain count never reaches zero.

Resolution Approaches:

  1. Weak Target Proxy: Use an intermediate forwarding object that holds a weak reference to the real target.
  2. Block-based Timer with [weak self]: Use Timer.scheduledTimer(withTimeInterval:repeats:block:) and explicitly call timer.invalidate() when done.
  3. Swift Concurrency Task: Replace Timer with a managed Task utilizing Task.sleep and cancel it on cleanup.

Code Example:

// Approach 1: Weak Proxy Pattern
final class WeakTimerProxy: NSObject {
    private weak var target: AnyObject?
    private let action: (AnyObject) -> Void
    
    init(target: AnyObject, action: @escaping (AnyObject) -> Void) {
        self.target = target
        self.action = action
        super.init()
    }
    
    @objc func timerFired() {
        if let target = target {
            action(target)
        }
    }
}

// Approach 2: Modern Swift Concurrency Task
final class PollingService {
    private var pollingTask: Task<Void, Never>?
    
    func startPolling() {
        pollingTask = Task { [weak self] in
            while !Task.isCancelled {
                try? await Task.sleep(nanoseconds: 5_000_000_000) // 5s
                guard !Task.isCancelled, let self else { break }
                await self.fetchLatestData()
            }
        }
    }
    
    func stopPolling() {
        pollingTask?.cancel()
        pollingTask = nil
    }
    
    deinit {
        pollingTask?.cancel()
    }
    
    private func fetchLatestData() async { /* ... */ }
}

Key Points:

  • Never rely on deinit to call timer.invalidate() if the timer strongly references self, because deinit will never trigger
  • Invalidate timers in viewWillDisappear, viewDidDisappear, or lifecycle cleanup hooks

Rate your understanding:

Ready to practice more Swift?

Test yourself with our interactive quiz mode or browse all curated questions for this topic.