- Timer Patterns Reference
- Complete API reference for iOS timer mechanisms. For decision trees and crash prevention, see
- axiom-timer-patterns
- .
- Part 1: Timer API
- Timer.scheduledTimer (Block-Based)
- // Most common — block-based, auto-added to current RunLoop
- let
- timer
- =
- Timer
- .
- scheduledTimer
- (
- withTimeInterval
- :
- 1.0
- ,
- repeats
- :
- true
- )
- {
- [
- weak
- self
- ]
- _
- in
- self
- ?
- .
- updateProgress
- (
- )
- }
- Key detail
- Added to .default RunLoop mode. Stops during scrolling. See Part 1 RunLoop modes table below. Timer.scheduledTimer (Selector-Based) // Objective-C style — RETAINS TARGET (leak risk) let timer = Timer . scheduledTimer ( timeInterval : 1.0 , target : self , // Timer retains self! selector :
selector
- (
- update
- )
- ,
- userInfo
- :
- nil
- ,
- repeats
- :
- true
- )
- Danger
-
- This API retains
- target
- . If
- self
- also holds the timer, you have a retain cycle. The block-based API with
- [weak self]
- is always safer.
- Timer.init (Manual RunLoop Addition)
- // Create timer without adding to RunLoop
- let
- timer
- =
- Timer
- (
- timeInterval
- :
- 1.0
- ,
- repeats
- :
- true
- )
- {
- [
- weak
- self
- ]
- _
- in
- self
- ?
- .
- updateProgress
- (
- )
- }
- // Add to specific RunLoop mode
- RunLoop
- .
- current
- .
- add
- (
- timer
- ,
- forMode
- :
- .
- common
- )
- // Survives scrolling
- timer.tolerance
- timer
- .
- tolerance
- =
- 0.1
- // Allow 100ms flexibility for system coalescing
- System batches timers with similar fire dates when tolerance is set. Minimum recommended: 10% of interval. Reduces CPU wakes and energy consumption.
- RunLoop Modes
- Mode
- Constant
- When Active
- Timer Fires?
- Default
- .default
- /
- RunLoop.Mode.default
- Normal user interaction
- Yes
- Tracking
- .tracking
- /
- RunLoop.Mode.tracking
- Scroll/drag gesture active
- Only if added to
- .common
- Common
- .common
- /
- RunLoop.Mode.common
- Pseudo-mode (default + tracking)
- Yes (always)
- timer.invalidate()
- timer
- .
- invalidate
- (
- )
- // Stops timer, removes from RunLoop
- // Timer is NOT reusable after invalidate — create a new one
- timer
- =
- nil
- // Release reference
- Key detail
- :
- invalidate()
- must be called from the same thread that created the timer (usually main thread).
- timer.isValid
- if
- timer
- .
- isValid
- {
- // Timer is still active
- }
- Returns
- false
- after
- invalidate()
- or after a non-repeating timer fires.
- Timer.publish (Combine)
- Timer
- .
- publish
- (
- every
- :
- 1.0
- ,
- tolerance
- :
- 0.1
- ,
- on
- :
- .
- main
- ,
- in
- :
- .
- common
- )
- .
- autoconnect
- (
- )
- .
- sink
- {
- [
- weak
- self
- ]
- _
- in
- self
- ?
- .
- updateProgress
- (
- )
- }
- .
- store
- (
- in
- :
- &
- cancellables
- )
- See Part 3 for full Combine timer details.
- Part 2: DispatchSourceTimer API
- Creation
- // Create timer source on a specific queue
- let
- queue
- =
- DispatchQueue
- (
- label
- :
- "com.app.timer"
- )
- let
- timer
- =
- DispatchSource
- .
- makeTimerSource
- (
- flags
- :
- [
- ]
- ,
- queue
- :
- queue
- )
- flags
-
- Usually empty (
- []
- ). Use
- .strict
- for precise timing (disables system coalescing, higher energy cost).
- Schedule
- // Relative deadline (monotonic clock)
- timer
- .
- schedule
- (
- deadline
- :
- .
- now
- (
- )
- +
- 1.0
- ,
- // First fire
- repeating
- :
- .
- seconds
- (
- 1
- )
- ,
- // Interval
- leeway
- :
- .
- milliseconds
- (
- 100
- )
- // Tolerance (like Timer.tolerance)
- )
- // Wall clock deadline (survives device sleep)
- timer
- .
- schedule
- (
- wallDeadline
- :
- .
- now
- (
- )
- +
- 1.0
- ,
- repeating
- :
- .
- seconds
- (
- 1
- )
- ,
- leeway
- :
- .
- milliseconds
- (
- 100
- )
- )
- deadline vs wallDeadline
- :
- deadline
- uses monotonic clock (pauses when device sleeps).
- wallDeadline
- uses wall clock (continues across sleep). Use
- deadline
- for most cases.
- Event Handler
- timer
- .
- setEventHandler
- {
- [
- weak
- self
- ]
- in
- self
- ?
- .
- performWork
- (
- )
- }
- Before cancel
-
- Set handler to nil to break retain cycles:
- timer
- .
- setEventHandler
- (
- handler
- :
- nil
- )
- timer
- .
- cancel
- (
- )
- Lifecycle Methods
- timer
- .
- activate
- (
- )
- // Start — can only call ONCE (idle → running)
- timer
- .
- suspend
- (
- )
- // Pause (running → suspended)
- timer
- .
- resume
- (
- )
- // Unpause (suspended → running)
- timer
- .
- cancel
- (
- )
- // Stop permanently (must NOT be suspended)
- State Machine Lifecycle
- activate()
- idle ──────────────► running
- │ ▲
- suspend() │ │ resume()
- ▼ │
- suspended
- │
- resume() + cancel()
- │
- ▼
- cancelled
- Critical rules
- :
- activate()
- can only be called once (idle → running)
- cancel()
- requires non-suspended state (resume first if suspended)
- cancelled
- is terminal — no further operations allowed
- Dealloc requires non-suspended state (cancel first if needed)
- Leeway (Tolerance)
- // Leeway values
- timer
- .
- schedule
- (
- deadline
- :
- .
- now
- (
- )
- ,
- repeating
- :
- 1.0
- ,
- leeway
- :
- .
- milliseconds
- (
- 100
- )
- )
- timer
- .
- schedule
- (
- deadline
- :
- .
- now
- (
- )
- ,
- repeating
- :
- 1.0
- ,
- leeway
- :
- .
- seconds
- (
- 1
- )
- )
- timer
- .
- schedule
- (
- deadline
- :
- .
- now
- (
- )
- ,
- repeating
- :
- 1.0
- ,
- leeway
- :
- .
- never
- )
- // Strict — high energy
- Leeway is the DispatchSourceTimer equivalent of
- Timer.tolerance
- . Allows system to coalesce timer firings for energy efficiency.
- End-to-End Example
- Complete DispatchSourceTimer lifecycle in one block:
- let
- queue
- =
- DispatchQueue
- (
- label
- :
- "com.app.polling"
- )
- let
- timer
- =
- DispatchSource
- .
- makeTimerSource
- (
- queue
- :
- queue
- )
- timer
- .
- schedule
- (
- deadline
- :
- .
- now
- (
- )
- +
- 1.0
- ,
- repeating
- :
- .
- seconds
- (
- 5
- )
- ,
- leeway
- :
- .
- milliseconds
- (
- 500
- )
- )
- timer
- .
- setEventHandler
- {
- [
- weak
- self
- ]
- in
- self
- ?
- .
- fetchUpdates
- (
- )
- }
- timer
- .
- activate
- (
- )
- // idle → running
- // Later — pause:
- timer
- .
- suspend
- (
- )
- // running → suspended
- // Later — resume:
- timer
- .
- resume
- (
- )
- // suspended → running
- // Cleanup — MUST resume before cancel if suspended:
- timer
- .
- setEventHandler
- (
- handler
- :
- nil
- )
- // Break retain cycles
- timer
- .
- resume
- (
- )
- // Ensure non-suspended state
- timer
- .
- cancel
- (
- )
- // running → cancelled (terminal)
- For a safe wrapper that prevents all crash patterns, see
- axiom-timer-patterns
- Part 4: SafeDispatchTimer.
- Part 3: Combine Timer
- Timer.publish
- import
- Combine
- // Create publisher — RunLoop mode matters here too
- let
- publisher
- =
- Timer
- .
- publish
- (
- every
- :
- 1.0
- ,
- // Interval
- tolerance
- :
- 0.1
- ,
- // Optional tolerance
- on
- :
- .
- main
- ,
- // RunLoop
- in
- :
- .
- common
- // Mode — use .common to survive scrolling
- )
- .autoconnect()
- // Starts immediately when first subscriber attaches
- Timer
- .
- publish
- (
- every
- :
- 1.0
- ,
- on
- :
- .
- main
- ,
- in
- :
- .
- common
- )
- .
- autoconnect
- (
- )
- .
- sink
- {
- date
- in
- (
- "Fired at
- (
- date
- )
- "
- )
- }
- .
- store
- (
- in
- :
- &
- cancellables
- )
- .connect() (Manual Start)
- // Manual control over when timer starts
- let
- timerPublisher
- =
- Timer
- .
- publish
- (
- every
- :
- 1.0
- ,
- on
- :
- .
- main
- ,
- in
- :
- .
- common
- )
- let
- cancellable
- =
- timerPublisher
- .
- sink
- {
- date
- in
- (
- "Fired at
- (
- date
- )
- "
- )
- }
- // Start later
- let
- connection
- =
- timerPublisher
- .
- connect
- (
- )
- // Stop
- connection
- .
- cancel
- (
- )
- Cancellation
- // Via AnyCancellable storage — cancelled when Set is cleared or object deallocs
- private
- var
- cancellables
- =
- Set
- <
- AnyCancellable
- >
- (
- )
- // Manual cancellation
- cancellables
- .
- removeAll
- (
- )
- // Cancels all subscriptions
- SwiftUI Integration
- class
- TimerViewModel
- :
- ObservableObject
- {
- @Published
- var
- elapsed
- :
- Int
- =
- 0
- private
- var
- cancellables
- =
- Set
- <
- AnyCancellable
- >
- (
- )
- func
- start
- (
- )
- {
- Timer
- .
- publish
- (
- every
- :
- 1.0
- ,
- tolerance
- :
- 0.1
- ,
- on
- :
- .
- main
- ,
- in
- :
- .
- common
- )
- .
- autoconnect
- (
- )
- .
- sink
- {
- [
- weak
- self
- ]
- _
- in
- self
- ?
- .
- elapsed
- +=
- 1
- }
- .
- store
- (
- in
- :
- &
- cancellables
- )
- }
- func
- stop
- (
- )
- {
- cancellables
- .
- removeAll
- (
- )
- }
- }
- Part 4: AsyncTimerSequence (Swift Concurrency)
- ContinuousClock.timer
- // Monotonic clock — does NOT pause when app suspends
- for
- await
- _
- in
- ContinuousClock
- (
- )
- .
- timer
- (
- interval
- :
- .
- seconds
- (
- 1
- )
- )
- {
- await
- updateData
- (
- )
- }
- // Loop exits when task is cancelled
- SuspendingClock.timer
- // Suspending clock — pauses when app suspends
- for
- await
- _
- in
- SuspendingClock
- (
- )
- .
- timer
- (
- interval
- :
- .
- seconds
- (
- 1
- )
- )
- {
- await
- processItem
- (
- )
- }
- ContinuousClock vs SuspendingClock
- :
- ContinuousClock
-
- Time keeps advancing during app suspension. Use for absolute timing.
- SuspendingClock
- Time pauses when app suspends. Use for "user-perceived" timing. Task Cancellation // Timer automatically stops when task is cancelled let timerTask = Task { for await _ in ContinuousClock ( ) . timer ( interval : . seconds ( 1 ) ) { await fetchLatestData ( ) } } // Later: cancel the timer timerTask . cancel ( ) Background Polling with Structured Concurrency func startPolling ( ) async { do { for try await _ in ContinuousClock ( ) . timer ( interval : . seconds ( 30 ) ) { try Task . checkCancellation ( ) let data = try await api . fetchUpdates ( ) await MainActor . run { updateUI ( with : data ) } } } catch is CancellationError { // Clean exit } catch { // Handle fetch error } } Part 5: Task.sleep Alternatives One-Shot Delay // Simple delay — NOT a timer try await Task . sleep ( for : . seconds ( 1 ) ) // Deadline-based try await Task . sleep ( until : . now + . seconds ( 1 ) , clock : . continuous ) When to Use Sleep vs Timer Need Use One-shot delay before action Task.sleep(for:) Repeating action ContinuousClock().timer(interval:) Delay with cancellation Task.sleep(for:) in a Task Retry with backoff Task.sleep(for:) in a loop Retry with Exponential Backoff func fetchWithRetry ( maxAttempts : Int = 3 ) async throws -> Data { var delay : Duration = . seconds ( 1 ) for attempt in 1 ... maxAttempts { do { return try await api . fetch ( ) } catch where attempt < maxAttempts { try await Task . sleep ( for : delay ) delay *= 2 // Exponential backoff } } throw FetchError . maxRetriesExceeded } Part 6: LLDB Timer Inspection Timer (NSTimer) Commands
Check if timer is still valid
po timer.isValid
See next fire date
po timer.fireDate
See timer interval
po timer.timeInterval
Force RunLoop iteration (may trigger timer)
expression -l objc -- (void)[[NSRunLoop mainRunLoop] run] DispatchSourceTimer Commands
Inspect dispatch source
po timer
Break on dispatch source cancel (all sources)
breakpoint set -n dispatch_source_cancel
Break on EXC_BAD_INSTRUCTION to catch timer crashes
(Xcode does this automatically for Swift runtime errors)
Check if a DispatchSource is cancelled
expression -l objc -- (long)dispatch_source_testcancel((void*)timer) General Timer Debugging
List all timers on the main RunLoop
expression -l objc -- (void)CFRunLoopGetMain()
Break when any Timer fires
breakpoint set -S "scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:" Part 7: Platform Availability Matrix API iOS macOS watchOS tvOS Timer 2.0+ 10.0+ 2.0+ 9.0+ DispatchSourceTimer 8.0+ (GCD) 10.10+ 2.0+ 9.0+ Timer.publish (Combine) 13.0+ 10.15+ 6.0+ 13.0+ AsyncTimerSequence 16.0+ 13.0+ 9.0+ 16.0+ Task.sleep 13.0+ 10.15+ 6.0+ 13.0+