axiom-timer-patterns-ref

Timer Patterns Reference

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "axiom-timer-patterns-ref" with this command: npx skills add charleswiltgen/axiom/charleswiltgen-axiom-axiom-timer-patterns-ref

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 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: &#x26;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+

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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

axiom-vision

No summary provided by upstream source.

Repository SourceNeeds Review
General

axiom-swiftdata

No summary provided by upstream source.

Repository SourceNeeds Review
General

axiom-swiftui-26-ref

No summary provided by upstream source.

Repository SourceNeeds Review