Add scan flow MVP and local Axiom skill workspace
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.
This commit is contained in:
7
.claude/skills/axiom-timer-patterns-ref/.openskills.json
Normal file
7
.claude/skills/axiom-timer-patterns-ref/.openskills.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"source": "CharlesWiltgen/Axiom",
|
||||
"sourceType": "git",
|
||||
"repoUrl": "https://github.com/CharlesWiltgen/Axiom",
|
||||
"subpath": "axiom-codex/skills/axiom-timer-patterns-ref",
|
||||
"installedAt": "2026-04-12T08:06:53.249Z"
|
||||
}
|
||||
470
.claude/skills/axiom-timer-patterns-ref/SKILL.md
Normal file
470
.claude/skills/axiom-timer-patterns-ref/SKILL.md
Normal file
@@ -0,0 +1,470 @@
|
||||
---
|
||||
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
|
||||
@@ -0,0 +1,3 @@
|
||||
interface:
|
||||
display_name: "Timer Patterns Reference"
|
||||
short_description: "Timer, DispatchSourceTimer, Combine Timer.publish, AsyncTimerSequence, Task.sleep API reference with lifecycle diagra..."
|
||||
Reference in New Issue
Block a user