mcpField
A
axiom-timer-patterns-ref

axiom-timer-patterns-ref

github.com/charleswiltgen開発1.2k87 回インストール今日更新
リポジトリ

axiom-timer-patterns-ref について

Swift開発者がiOSのタイマーAPIを選び実装するためのリファレンススキルです。Timer、DispatchSourceTimer、Combine、並行処理のタイマーに加え、ライフサイクルやデバッグも解説します。

主な機能

  1. 01TimerとRunLoopモードの解説
  2. 02DispatchSourceTimerのライフサイクルと猶予時間
  3. 03Combineタイマーの発行とキャンセル
  4. 04非同期タイマーとTask.sleepのパターン
  5. 05LLDBでのタイマー確認コマンド

活用シーン

  1. 01定期処理に使うタイマーAPIの選択
  2. 02タイマーの循環参照やライフサイクルエラーの回避
  3. 03キャンセル可能な非同期ポーリングの実装
  4. 04LLDBによるタイマー動作のデバッグ

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/charleswiltgen/axiom --skill axiom-timer-patterns-ref

インストール後、Claude Code で/axiom-timer-patterns-refを入力して読み込まれたか確認します。

詳細

タイマーパターンのリファレンス

iOS のタイマー機構に関する完全な API リファレンスです。判断ツリーとクラッシュ防止については、axiom-timer-patterns を参照してください。


Part 1:タイマー API

Timer.scheduledTimer(ブロックベース)
// Most common — block-based, auto-added to current RunLoop
let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
    self?.updateProgress()
}

重要な詳細:.default RunLoop モードに追加されます。スクロール中は停止します。詳しくは、以下の Part 1 の RunLoop モードの表を参照してください。

Timer.scheduledTimer(セレクターベース)
// 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
)

危険:この API は target を保持します。self もタイマーを保持している場合、保持サイクルが発生します。[weak self] を使うブロックベース API のほうが常に安全です。

Timer.init(RunLoop への手動追加)
// 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

許容範囲を設定すると、システムは発火時刻が近いタイマーをまとめて処理します。推奨される最小値は、インターバルの 10% です。CPU のウェイクアップとエネルギー消費を削減できます。

RunLoop モード
モード定数アクティブになる状況タイマーは発火するか
Default.default / RunLoop.Mode.default通常のユーザー操作はい
Tracking.tracking / RunLoop.Mode.trackingスクロール/ドラッグジェスチャーがアクティブ.common に追加した場合のみ
Common.common / RunLoop.Mode.common疑似モード(default + tracking)はい(常時)
timer.invalidate()
timer.invalidate()  // Stops timer, removes from RunLoop
// Timer is NOT reusable after invalidate — create a new one
timer = nil          // Release reference

重要な詳細:invalidate() は、タイマーを作成したスレッド(通常はメインスレッド)から呼び出す必要があります。

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

invalidate() の呼び出し後、または繰り返しなしのタイマーが発火した後は、false を返します。

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)

Combine タイマーの詳細については、Part 3 を参照してください。


Part 2:DispatchSourceTimer API

作成
// Create timer source on a specific queue
let queue = DispatchQueue(label: "com.app.timer")
let timer = DispatchSource.makeTimerSource(flags: [], queue: queue)

flags:通常は空([])です。正確なタイミングが必要な場合は .strict を使います(システムによるタイマー処理の統合が無効になり、エネルギー消費が増加します)。

スケジュール
// 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 と wallDeadline の違い:deadline は単調時計を使います(デバイスのスリープ中は停止します)。wallDeadline は壁時計を使います(スリープ中も進み続けます)。ほとんどの場合は deadline を使ってください。

イベントハンドラー
timer.setEventHandler { [weak self] in
    self?.performWork()
}

キャンセルする前に:保持サイクルを解消するため、ハンドラーを nil に設定します。

timer.setEventHandler(handler: nil)
timer.cancel()
ライフサイクルメソッド
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)
ステートマシンのライフサイクル
                    activate()
        idle ──────────────► running
                               │  ▲
                    suspend()  │  │  resume()
                               ▼  │
                            suspended
                               │
                    resume() + cancel()
                               │
                               ▼
                           cancelled

重要なルール:

  • activate() は一度だけ呼び出せます(idle → running)
  • cancel() はサスペンドされていない状態で呼び出す必要があります(サスペンド中の場合は、先に再開してください)
  • cancelled は終了状態です。それ以降の操作はできません
  • 解放時はサスペンドされていない状態である必要があります(必要に応じて、先にキャンセルしてください)
Leeway(許容範囲)
// 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 は Timer.tolerance に相当する DispatchSourceTimer の機能です。エネルギー効率を高めるため、システムがタイマーの発火をまとめて処理できるようにします。

最初から最後までの例

DispatchSourceTimer のライフサイクル全体を 1 つのブロックにまとめた例です。

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)

すべてのクラッシュパターンを防ぐ安全なラッパーについては、axiom-timer-patterns の Part 4:SafeDispatchTimer を参照してください。


Part 3:Combine タイマー

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 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()
キャンセル
// Via AnyCancellable storage — cancelled when Set is cleared or object deallocs
private var cancellables = Set<AnyCancellable>()

// Manual cancellation
cancellables.removeAll()  // Cancels all subscriptions
SwiftUI との統合
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 と SuspendingClock の違い:

  • ContinuousClock:アプリのサスペンド中も時間が進み続けます。絶対時刻に基づく計測に使います。
  • SuspendingClock:アプリのサスペンド中は時間が停止します。「ユーザーが感じる」時間の計測に使います。
タスクのキャンセル
// 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()
構造化並行処理によるバックグラウンドのポーリング
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 の代替手段

1 回限りの遅延
// Simple delay — NOT a timer
try await Task.sleep(for: .seconds(1))

// Deadline-based
try await Task.sleep(until: .now + .seconds(1), clock: .continuous)
Sleep と Timer の使い分け
必要な処理使用するもの
アクションの前に 1 回だけ遅延させるTask.sleep(for:)
繰り返し実行するアクションContinuousClock().timer(interval:)
キャンセル可能な遅延Task 内の Task.sleep(for:)
バックオフを伴う再試行ループ内の Task.sleep(for:)
指数バックオフによる再試行
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(NSTimer)コマンド
# 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 コマンド
# 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)
タイマーの一般的なデバッグ
# 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:"

パート 7:プラットフォーム対応状況一覧

APIiOSmacOSwatchOStvOS
Timer2.0+10.0+2.0+9.0+
DispatchSourceTimer8.0+ (GCD)10.10+2.0+9.0+
Timer.publish (Combine)13.0+10.15+6.0+13.0+
AsyncTimerSequence16.0+13.0+9.0+16.0+
Task.sleep13.0+10.15+6.0+13.0+

関連スキル

  • axiom-timer-patterns — 判断ツリー、クラッシュパターン、SafeDispatchTimer ラッパー
  • axiom-energy — エネルギー最適化としてのタイマー許容範囲(パターン 1)
  • axiom-energy-ref — WWDC のコード例を使ったタイマー効率化 API
  • axiom-memory-debugging — パターン 1 のメモリーリークとしてのタイマー

リソース

スキル:axiom-timer-patterns、axiom-energy-ref、axiom-memory-debugging

コメント