Skillshub axiom-timer-patterns-ref
Timer, DispatchSourceTimer, Combine Timer.publish, AsyncTimerSequence, Task.sleep API reference with lifecycle diagrams, RunLoop modes, and platform availability
git clone https://github.com/ComeOnOliver/skillshub
T=$(mktemp -d) && git clone --depth=1 https://github.com/ComeOnOliver/skillshub "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/CharlesWiltgen/Axiom/axiom-timer-patterns-ref" ~/.claude/skills/comeonoliver-skillshub-axiom-timer-patterns-ref && rm -rf "$T"
skills/CharlesWiltgen/Axiom/axiom-timer-patterns-ref/SKILL.mdTimer 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 | / | Normal user interaction | Yes |
| Tracking | / | Scroll/drag gesture active | Only if added to |
| 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:
can only be called once (idle → running)activate()
requires non-suspended state (resume first if suspended)cancel()
is terminal — no further operations allowedcancelled- 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 print("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 print("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:
: Time keeps advancing during app suspension. Use for absolute timing.ContinuousClock
: Time pauses when app suspends. Use for "user-perceived" timing.SuspendingClock
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 | |
| Repeating action | |
| Delay with cancellation | in a Task |
| Retry with backoff | 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+ |
Related Skills
— Decision trees, crash patterns, SafeDispatchTimer wrapperaxiom-timer-patterns
— Timer tolerance as energy optimization (Pattern 1)axiom-energy
— Timer efficiency APIs with WWDC code examplesaxiom-energy-ref
— Timer as Pattern 1 memory leakaxiom-memory-debugging
Resources
Skills: axiom-timer-patterns, axiom-energy-ref, axiom-memory-debugging