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 schedules a callback on a run loop at an interval you choose, once or repeating. Firing is approximate, because a busy run loop delays ticks, and it has no relationship to the display's refresh. Use it for periodic work that doesn't need to run once per frame: polling, debouncing input, and housekeeping that runs every few seconds.
CADisplayLink is driven by the display, firing in sync with screen updates, typically once per frame at the current refresh rate (60 Hz, or up to 120 Hz on ProMotion). Each tick gives you timestamp, targetTimestamp, and duration for frame timing. Use it when output must track the screen: custom per-frame drawing, smooth progress, or sampling a value every frame.
Pick Timer when the schedule is wall-clock or coarse and not tied to drawing. Pick CADisplayLink when work should line up with frames. For most animations, prefer UIViewPropertyAnimator, Core Animation, or SwiftUI's animation system over driving transforms by hand from either one.
Both retain their target when you create them with a target and selector, so prefer the block-based Timer with [weak self]. Invalidate either one when the work is done, such as in viewDidDisappear. Don't wait for deinit, because a timer that retains its target keeps deinit from ever running, so you leak the owner and burn CPU.
Principles
Timerfires at an interval on the run loop and isn't synchronized to the display.CADisplayLinkfires once per frame, paced by the display, with timestamps for frame timing.- Prefer higher-level animation APIs when they express the effect, and reach for
CADisplayLinkonly for frame-synchronized custom stepping. - Both retain a target passed with a selector, so invalidate them when the work ends rather than trusting
deinit.
Build a Timer and add it to RunLoop.main in .common mode so it keeps firing while a scroll view is tracking:
let timer = Timer(timeInterval: 0.25, repeats: true) { [weak self] _ in
self?.tick()
}
RunLoop.main.add(timer, forMode: .common)
Hook CADisplayLink to a #selector, compute elapsed time from the timestamps so missed frames don't make progress drift, and call 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
- A
Timeradded in the default run loop mode pauses while a scroll view is tracking, which surprises people, so add it in.common. - In SwiftUI,
TimelineView,withAnimation, andPhaseAnimatorcover most periodic and transition effects without a manualCADisplayLink.



