AlarmKit使ってみた

に公開

はじめに

WWDC2025で発表されたAlarmKitの実装手順を、できるだけ手軽に試せるように整理しました。
本記事では、AlarmKit を導入するための主要なステップを順に解説します。

完成図

(アラーム時)ホーム画面 (アラーム時)ロック画面
(カウント時)ホーム画面 (カウント時)ロック画面

事前準備

まず、AlarmKit を使うために必要なモジュールをインポートします。

import AlarmKit

次に、Info.plist に次のキーを追加する必要があります

NSAlarmKitUsageDescription

value には「アラーム機能を使用する目的」を記入します。説明文は、ユーザーに権限確認ポップアップで表示されます。

権限設定

初回のアラーム設定時に自動でポップアップが表示されます。

AlarmManager.shared.requestAuthorization()で手動設定も可能です。

[参考]権限許可コード
func requestAuthorization() async -> Bool {
    do {
        let manager = AlarmManager.shared
        let status = try await manager.requestAuthorization()
        return status == .authorized
    } catch {
        print("can't request alarm authorization")
        return false
    }
}

権限の確認は
AlarmManager.shared.authorizationState
権限確認していない状態.notDetermined
権限の許可がある状態.authorized
権限が拒否されている状態.denied
を確認できます。

[参考]権限確認コード
func checkAuthorization() async -> Bool {
    switch AlarmManager.shared.authorizationState {
    case .notDetermined:
        // requestAuthorization()は[参考]権限許可コードを参照
        let status = await requestAuthorization()
        return status
    case .authorized:
        return true
    case .denied:
        return false
    default:
        return false
    }
}

UIの設定

アラームが鳴った時にロック画面や通知バーに表示されるボタンなどのUIを定義する必要があります。

//ストップボタンの定義
//systemImageはDynamicIslandで表示される際に使用される。
let stopButton = AlarmButton(
    text: "Dismiss",
    textColor: .white,
    systemImageName: "stop.circle"
)
//リピートボタンの定義
let repeteButton = AlarmButton(
    text: "Repete",
    textColor: .white,
    systemImageName: "repeat.circle"
)
//画面の定義
//今回はsecondaryButtonにリピート機能を割り当てる
let alertPresentation = AlarmPresentation.Alert(
    title: "test!!!",
    stopButton: stopButton,
    secondaryButton: repeteButton,
    secondaryButtonBehavior: .countdown
)
//アラームの属性の設定
//tintColorでテーマカラーを設定する
let attributes = AlarmAttributes<MyAlarmMetadata>(
    presentation: AlarmPresentation(
        alert: alertPresentation
    ),
    tintColor: .green
)

アラーム情報の保持

アラームそのもののスケジューリングや発火はシステム側(AlarmKit)に委ねますが、アプリ側でも「どのアラームを操作するか」を識別できるようにしておく必要があります。
そのため、アラームを一意に特定するためのメタデータ(例: アラームIDなど)を保持する構造体を定義しておきます。
この情報があることで、すでに設定済みのアラームに対して以下のような操作が可能になります。
• 一時停止(pause)
• 再開(resume)
• 停止(stop)
また、このメタデータはウィジェットや Live Activity などの外部 UI からも使われます。
たとえばロック画面上の Live Activity に「どのアラームの残り時間を表示するか」「どのアラームを一時停止ボタンで操作するか」を判断するためには、対象のアラームを識別できる情報が必要です。

一方で、「ただアラームを鳴らすだけで、あとから制御したり表示したりしない」という用途であれば、メタデータの中身は空でも問題ありません。

//アラームを識別するためのやつ
nonisolated struct MyAlarmMetadata: AlarmMetadata {
    let alarmID: UUID
    let title: String
}

アラーム機能の実装

固定の日時/相対日時、どちらのスケジューリングにも対応可能です。
この二つは、タイムゾーンの変更にどう対応するかが変わります。
具体的には
fixedの場合にはタイムゾーンに影響されない、タイムゾーンが変わっても指定した時間にアラームがなります。
一方relativeの場合にはローカルタイムに基づいたアラームがなる時間が調整されます。

固定の日時の場合

let dateComponent = DateComponents(
    calendar: .current,
    year: 2025,
    month: 10,
    day: 15,
    hour: 23,
    minute: 51
)
let alarmDate = Calendar.current.date(from: dateComponent)!
let scheduleFixed = Alarm.Schedule.fixed(alarmDate)

//固定の日時の場合はscheduleに設定する
//attributesは先ほど設定したアラームを定義したもの
let alarmConfiguration = AlarmManager.AlarmConfiguration(
    schedule: scheduleFixed,
    attributes: attributes
)

AlarmConfiguration内ではsoundオプションで音の変更も可能ですが、今回は省略しています。

相対的な日時の場合

let time = Alarm.Schedule.Relative.Time(hour: 0, minute: 25)
let reccurrence = Alarm.Schedule.Relative.Recurrence.weekly([
    .monday,
    .wednesday,
    .friday
])
let scheduleInfo = Alarm.Schedule.Relative(time: time, repeats: reccurrence)
let schedule = Alarm.Schedule.relative(scheduleInfo)

これまで行なったアラームの設定を元に以下のコードでスケジュール登録を行います。

do {
    let alarm = try await AlarmManager.shared.schedule(
        id: UUID(),
        configuration: alarmConfiguration
    )
    print("アラームを設定しました: \(alarm)")
} catch {
    print("アラームの設定に失敗しました")
}

これで指定した時間にアラームが鳴るようになります。

カウントダウンの実装

固定日時に加え、設定した時間が経過した後にアラームを鳴らす「カウントダウン」機能も実装できます。UI設定・メタデータ保持の部分は前述と共通です。

let duration = Alarm.CountdownDuration(
    //アラームが鳴るまでのカウント時間(1分)
    preAlert: 60,
    //スヌーズ機能で次にアラームが鳴るまでのカウント時間(5分)
    postAlert: 5 * 60
)
let alarmConfiguration = AlarmManager.AlarmConfiguration(
    countdownDuration: duration,
    attributes: attributes
)

その後、スケジュール登録コードは固定日時の場合と同様です。

do {
    let alarm = try await AlarmManager.shared.schedule(
        id: UUID(),
        configuration: alarmConfiguration
    )
    print("アラームを設定しました: \(alarm)")
} catch {
    print("アラームの設定に失敗しました")
}

これでカウントダウン形式のアラームが設定できます。
ただし、現状の AlarmKit ではアプリ内でカウントダウンの経過時間をリアルタイムに取得・全て表示できるわけではなく、主に Live Activity/ウィジェットとの組み合わせで残り時間を提示する設計になっています。

LiveActivityの実装

2022年のWWDCで発表された機能でロック画面やウィジェットにアプリの進行中の情報をリアルタイムに表示することができる機能です。

事前準備

  • widget Extensionを追加する
    file Newtarget... から widget Extensionを追加する。

info.plistへの記載

Info.plist に以下を追加して値を Yes に設定します

NSSupportsLiveActivities

設定を忘れると、ウィジェットやロック画面上での Live Activity が動作しないので注意してください。

実装

今回はAlarmkitについての記事のためwidgetの実装は最低限の紹介にとどめます。
widget Extensionを追加した時に生成されるファイルを修正することで実装を進めます。
以下の記述を修正し、Alarmの情報をwidget側に伝えます。

〇〇LiveActivity.swift
- ActivityConfiguration(for: testLiveActivityAttributes.self)
+ ActivityConfiguration(for: AlarmAttributes<MyAlarmMetadata>.self)
〇〇LiveActivity.swift
struct CountDownMiniView: View {
    let context: ActivityViewContext<AlarmAttributes<MyAlarmMetadata>>
    var body: some View {
        if case let .countdown(countdown) = context.state.mode {
            //カウントダウン表示
            Text(
                timerInterval: Date.now ... countdown.fireDate
            )
        }
    }
}
[参考]LiveActivityのコード
〇〇LiveActivity.swift
import ActivityKit
import WidgetKit
import SwiftUI
import AlarmKit

struct CountdownTimerLiveActivityLiveActivity: Widget {
    var body: some WidgetConfiguration {
        ActivityConfiguration(for: AlarmAttributes<MyAlarmMetadata>.self) { context in
            VStack {
                Text("CountDown")
                CountDownView(context: context)
            }
            
        } dynamicIsland: { context in
            DynamicIsland {
                DynamicIslandExpandedRegion(.bottom) {
                    HStack {
                        CountDownView(context: context)
                             .font(.headline)
                         CountdownProgressView(state: context.state)
                             .frame(maxHeight: 30)
                     }
                }
            } compactLeading: {
                CountDownMiniView(context: context)
            } compactTrailing: {
                CountdownProgressView(state: context.state)
            } minimal: {
                CountDownMiniView(context: context)
            }
        }
    }
}
struct CountDownMiniView: View {
    let context: ActivityViewContext<AlarmAttributes<MyAlarmMetadata>>
    var body: some View {
        if case let .countdown(countdown) = context.state.mode {
            Text(
                timerInterval: Date.now ... countdown.fireDate
            )
        }
    }
}

struct CountDownView: View {
    let context: ActivityViewContext<AlarmAttributes<MyAlarmMetadata>>
    var body: some View {
        if case let .countdown(countdown) = context.state.mode {
            HStack(alignment: .center, spacing: 12) {
                VStack(alignment: .leading, spacing: 4) {
                    Text(context.attributes.metadata?.title ?? "Untitled")
                        .font(.subheadline)
                        .fontWeight(.semibold)
                    Text(
                        timerInterval: Date.now ... countdown.fireDate,
                        countsDown: true
                    )
                    .font(.largeTitle)
                    .monospacedDigit()
                }

                Spacer()

                VStack(alignment: .trailing) {
                    Image(systemName: "frying.pan.fill")
                        .foregroundColor(.gray)
                }

                HStack(spacing: 8) {
                    Button {
                        // pause
                    } label: {
                        Label("Pause", systemImage: "pause.fill")
                            .font(.caption2)
                            .imageScale(.small)
                            .padding(.horizontal, 8)
                            .padding(.vertical, 4)
                            .frame(height: 28)
                            .background(Color.orange.opacity(0.18), in: Capsule())
                    }
                    .buttonStyle(.plain)
                    .contentShape(Capsule())

                    Button {
                        // stop
                    } label: {
                        Label("Stop", systemImage: "stop.fill")
                            .font(.caption2)
                            .imageScale(.small)
                            .padding(.horizontal, 8)
                            .padding(.vertical, 4)
                            .frame(height: 28)
                            .background(Color.red.opacity(0.18), in: Capsule())
                    }
                    .buttonStyle(.plain)
                    .contentShape(Capsule())
                }
            }
            .padding(.horizontal)
            .frame(maxWidth: .infinity)
        }
    }
}

struct CountdownProgressView: View {
    let state: AlarmPresentationState
    var body: some View {
        if case let .countdown(countdown) = state.mode {
            ProgressView(
                timerInterval: Date.now ... countdown.fireDate,
                label: { EmptyView() },
                currentValueLabel: { Text("") }
            )
            .progressViewStyle(.circular)
        }
    }
}

アラームの操作

ここではアラームの操作に関するコードを紹介します。
以下のコードは、一時停止を行う例です。
AlarmManager.shared.pause(id: uuid)
このほかにも、.resume,.stopなどのメソッドを使用できます。
uuid には AlarmId を指定することで、特定のアラームを操作できます。

[参考]AppIntents一時停止のコード
struct PauseAlarmIntent: AppIntent {
    static var title: LocalizedStringResource = "アラームを一時停止"

    @Parameter(title: "Alarm ID")
    var alarmID: String?

    init() {
        self.alarmID = nil
    }

    init(alarmID: UUID?) {
        self.alarmID = alarmID?.uuidString
    }

    @MainActor
    func perform() async throws -> some IntentResult {
        guard let idString = alarmID, let uuid = UUID(uuidString: idString) else {
            print("無効なAlarm IDです。")
            return .result()
        }

        try AlarmManager.shared.pause(id: uuid)
        
        return .result()
    }
}

最後に、このAppIntentsをLive ActivityのButtonから呼び出すことで、ロック画面などから直接操作できるようになります。

[参考]LiveActivityのコード
〇〇LiveActivity.swift
HStack(spacing: 8) {
+ Button(intent: PauseAlarmIntent(alarmID: context.attributes.metadata?.alarmID)) {
        Label("Pause", systemImage: "pause.fill")
            .font(.caption2)
            .imageScale(.small)
            .padding(.horizontal, 8)
            .padding(.vertical, 4)
            .frame(height: 28)
            .background(Color.orange.opacity(0.18), in: Capsule())
    }
    .buttonStyle(.plain)
    .contentShape(Capsule())

終わりに

以上がAlarmKitを私自身が触れてまとめてみた内容になります。
わかりやすい記事になっていれば幸いです。
また、「ここ間違ってない?」とか「こここうしたほうがいいよ〜」などフィードバックもお待ちしてます!

参考文献

https://developer.apple.com/videos/play/wwdc2025/230/
https://nilcoalescing.com/blog/CountdownTimerWithAlarmKit/
https://qiita.com/Cychow/items/6ece2955e809ef136bc2

Discussion