---
name: axiom-timer-patterns-ref
description: Timer, DispatchSourceTimer, Combine Timer.publish, AsyncTimerSequence, Task.sleep API reference with lifecycle diagrams, RunLoop modes, and platform availability
license: MIT
metadata:
  version: "1.0.0"
  last-updated: "2026-02-26"
---

# 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)

```swift
// 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)

```swift
// 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)

```swift
// 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

```swift
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()

```swift
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

```swift
if timer.isValid {
    // Timer is still active
}
```

Returns `false` after `invalidate()` or after a non-repeating timer fires.

### Timer.publish (Combine)

```swift
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

```swift
// 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

```swift
// 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

```swift
timer.setEventHandler { [weak self] in
    self?.performWork()
}
```

**Before cancel**: Set handler to nil to break retain cycles:

```swift
timer.setEventHandler(handler: nil)
timer.cancel()
```

### Lifecycle Methods

```swift
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)

```swift
// 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:

```swift
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

```swift
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()

```swift
// Starts immediately when first subscriber attaches
Timer.publish(every: 1.0, on: .main, in: .common)
    .autoconnect()
    .sink { date in
        print("Fired at \(date)")
    }
    .store(in: &cancellables)
```

### .connect() (Manual Start)

```swift
// Manual control over when timer starts
let timerPublisher = Timer.publish(every: 1.0, on: .main, in: .common)
let cancellable = timerPublisher
    .sink { date in
        print("Fired at \(date)")
    }

// Start later
let connection = timerPublisher.connect()

// Stop
connection.cancel()
```

### Cancellation

```swift
// 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

```swift
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

```swift
// 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

```swift
// 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

```swift
// 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

```swift
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

```swift
// 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

```swift
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

```lldb
# 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

```lldb
# 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

```lldb
# 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+ |

---

## Related Skills

- `axiom-timer-patterns` — Decision trees, crash patterns, SafeDispatchTimer wrapper
- `axiom-energy` — Timer tolerance as energy optimization (Pattern 1)
- `axiom-energy-ref` — Timer efficiency APIs with WWDC code examples
- `axiom-memory-debugging` — Timer as Pattern 1 memory leak

## Resources

**Skills**: axiom-timer-patterns, axiom-energy-ref, axiom-memory-debugging
