Question 10
`Timer` vs `CADisplayLink`
What’s the difference between Timer and CADisplayLink? When would you pick one over the other for UI-related work?
Follow-ups
- What APIs might replace both for many animation cases today?
Answer outline
Timer (Foundation): Schedules a callback on a run loop at a time interval you choose (repeating or once). Firing is approximate—the run loop can delay ticks when busy, and Timer is not locked to the display’s refresh. Use it for periodic work that doesn’t need to run once per frame: polling, debouncing input, refresh timers, 'every N seconds' housekeeping.
CADisplayLink (QuartzCore): Driven by the display: it fires in sync with screen updates (typically once per frame at the current refresh rate, e.g. 60/120 Hz on ProMotion). You get timestamp, targetTimestamp, and duration for frame timing, ideal when output should track vsync: custom per-frame drawing, progress that should stay smooth with the display, or reading values each frame without drifting from what the user sees.
Pick Timer when the schedule is wall-clock or coarse ('every 0.5s'), not tied to drawing. Pick CADisplayLink when work should align with frames. For many animations, prefer UIViewPropertyAnimator, Core Animation, or SwiftUI’s animation system instead of driving transforms manually from either primitive.
Lifecycle: Both keep a strong reference to their target unless you use blocks with [weak self] or invalidate explicitly. Invalidate Timer and invalidate/remove CADisplayLink from the run loop in deinit / viewDidDisappear when done—otherwise leaks and CPU burn after the screen is gone.
Principles
- Timer = interval on the run loop, not synchronized to display refresh.
CADisplayLink= per frame, display-paced.- Prefer higher-level animation APIs when they express the effect; use
CADisplayLinkwhen you truly need frame-synchronized custom stepping.
Build a Timer, then add it to RunLoop.main in .common so it still fires while a UIScrollView is tracking (the default mode can pause during scroll).
let t = Timer(timeInterval: 0.25, repeats: true) { [weak self] _ in
self?.tick()
}
RunLoop.main.add(t, forMode: .common)
Hook CADisplayLink to #selector (or wrap in a block API on newer OS). For simulations, compute elapsed time from timestamps so missed frames do not make progress drift; invalidate() when done.
private var lastFrameTimestamp: CFTimeInterval?
displayLink = CADisplayLink(target: self, selector: #selector(step(_:)))
displayLink?.add(to: .main, forMode: .common)
@objc func step(_ link: CADisplayLink) {
let previous = lastFrameTimestamp ?? link.timestamp
let dt = link.timestamp - previous
lastFrameTimestamp = link.timestamp
advanceAnimation(by: dt)
}
Follow-up angles
- Scrolling: Without
.common, aTimertied to.defaultcan pause during scroll—often surprising. - SwiftUI:
TimelineView,withAnimation,PhaseAnimatorcover many periodic or transition effects without manualCADisplayLink.