Which work still runs while the run loop is in a tracking or custom mode, and what does “common modes” really contain?
Predict first
Which pieces of work run in a custom mode before and after it joins the common modes?
import Foundation
let custom = RunLoop.Mode("com.example.tracking")
nonisolated(unsafe) var log: [String] = []
func spin(_ mode: RunLoop.Mode, _ seconds: Double = 0.3) {
let end = Date(timeIntervalSinceNow: seconds)
while Date() < end { RunLoop.main.run(mode: mode, before: end) }
}
func report(_ label: String) { print(label, Set(log).sorted()); log.removeAll() }
RunLoop.main.add(Timer(timeInterval: 0.02, repeats: true) { _ in log.append("default-mode timer") }, forMode: .default)
RunLoop.main.add(Timer(timeInterval: 0.02, repeats: true) { _ in log.append("common-modes timer") }, forMode: .common)
DispatchQueue.main.async { log.append("main queue block") }
spin(custom); report("custom mode, not a common mode:")
CFRunLoopAddCommonMode(CFRunLoopGetMain(), CFRunLoopMode(custom.rawValue as CFString))
spin(custom); report("custom mode, now a common mode:")
spin(.default); report("default mode:")
- Nothing runs in the custom mode until it joins the common set; then the common timer and the main queue block run, but not the default-mode timer
- The main queue block and both timers run in any mode
- The default-mode timer runs in the custom mode
Mental model. A run loop in a mode is a venue with a guest list. A common source is added to every room that counts as “public”, and the post office window (the main queue) is open in every public room, but a custom room is private until you declare it public.
Q217 explains why a default-mode Timer stalls during scrolling. The complete picture is a table of what is registered for which mode. The check runs the main loop in a made-up mode that is not in the common set: nothing runs there, neither a default-mode timer, a common-modes timer, nor a DispatchQueue.main.async block. After adding the made-up mode to the main loop’s common-mode set (CFRunLoopAddCommonMode), spinning it runs the common-modes timer and the main-queue block but still not the default-mode timer. Back in the default mode, the default-mode and common-mode timers run. So the common modes are a named set of real modes (default and UI-tracking by default) that you can extend, a “common” source or timer is registered into every mode in the set, and the main dispatch queue is serviced as a common-mode source of the main loop.
Practical results: DispatchQueue.main.async keeps working during scrolling (it is serviced in tracking mode), which makes it the safe way to deliver results to the UI, whereas a default-mode timer, a perform(_:with:afterDelay:) and a default-mode NotificationCenter observer queue are paused until scrolling ends. The same logic holds on macOS during menu tracking and window resizing (.eventTracking, .modalPanel). It also explains modal run loops: a modal alert spins a private mode so that unrelated timers do not fire inside it, and why re-entrancy bugs appear when code calls RunLoop.run inside an event handler (other handlers run in the nested loop). The exact set of UIKit’s tracking modes is not documented beyond UITrackingRunLoopMode being a common mode; the check shows the mechanism on macOS, not UIKit’s private modes.
Registrations go to the modes they were made for; common-mode registrations go to every mode in the common set, including ones you add.
Common traps
- Assuming everything stops while the user scrolls. The main queue and common-mode sources keep running; only default-mode registrations pause.
- Spinning a nested run loop to wait for a result. It re-enters event handling and invites re-entrancy bugs; use
awaitor a completion.
They might follow up with
- How do I run a timer during scrolling? Add it with
RunLoop.main.add(timer, forMode: .common). - Why does the main queue work in tracking mode? It is registered in the common set rather than for the default mode only.
Related questions
- 6.3.2 What are run loop modes, and why does a Timer stop firing while the user scrolls?
- 6.3.3 What is the main-thread contract, and why does DispatchQueue.main.sync from the main thread crash?
- 6.10.1 Why does a Timer created on a background thread never fire, and when does a thread’s run loop return…
- 6.10.2 What are run loop observers, in which order do they fire, and what do frameworks use them for?
← What are run loop observers, in which order do they fire, and what do…