This snapshot establishes the camera-to-result recognition flow and related tests while checking in the project skill/docs assets required for the configured local tooling.
471 lines
12 KiB
Markdown
471 lines
12 KiB
Markdown
---
|
|
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
|