← All topics/UIKit & SwiftUI internals

Practical interview questions

Scenario-style prompts with sample answer outlines. Focus is on how you would design and reason in real codebases.

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 CADisplayLink when 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).

Timer — coarse / repeating
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.

CADisplayLink — per frame
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, a Timer tied to .default can pause during scroll—often surprising.
  • SwiftUI: TimelineView, withAnimation, PhaseAnimator cover many periodic or transition effects without manual CADisplayLink.