Highlight
iOS 26 では AlarmKit によって、「サイレントモードや集中モードを貫通する」アラーム機能がサードパーティ App にも開放されました。Live Activity と組み合わせてロック画面や Dynamic Island にカウントダウンを表示でき、App Intents を使ってボタンの挙動をカスタマイズすることも可能です。
核心となる内容
サードパーティ App はこれまで、ある制約に阻まれてきました。ローカル通知はサイレントモードや集中モードに抑え込まれてしまい、時間が来てもユーザーには聞こえないのです。料理のタイマー、起床アラーム、ランニングのペース通知のような「必ず鳴ってほしい」シーンでは、開発者はユーザーにサイレントを切ってもらうよう促すか、Push 通知 + バックグラウンド音声再生というグレーな手段を取るしかありませんでした。システムレベルのアラーム機能は、これまで Apple 純正の「時計」App でしか使えなかったのです。
iOS 26 の AlarmKit は、この壁を取り払いました。フレームワークはシステム純正のアラームと同じ優先度を提供し、鳴動時にサイレントモードと現在の Focus を貫通します。ロック画面、Dynamic Island、StandBy、Apple Watch での表示にも対応します。開発者は Info.plist に NSAlarmKitUsageDescription を宣言し、ユーザーが許可すれば、App は AlarmManager.shared.schedule(id:configuration:) でアラームをスケジュールできます。アラームは 4 つの要素から構成されます。カウントダウン時間、スケジューリングポリシー(固定時刻または相対時刻の繰り返し)、外観の設定(タイトル、ボタン、カラー)、そしてオプションのカスタムサウンドです。Live Activity は装飾ではなく必須です。アラームがカウントダウン機能を持つ場合、App は対応する Live Activity を必ず実装しなければなりません。実装しない場合はシステムのフォールバック外観が使われます。
詳細
アラームをスケジュールする前に、まず認可状態をチェックする必要があります。AlarmManager.shared.authorizationState は .notDetermined / .authorized / .denied の 3 つの値を返します(02:41)。
import AlarmKit
func checkAuthorization() {
switch AlarmManager.shared.authorizationState {
case .notDetermined:
// Manually request authorization
case .authorized:
// Proceed with scheduling
case .denied:
// Inform status is not authorized
}
}
ポイント:
AlarmManager.sharedはシングルトンの入口で、スケジュール、クエリ、キャンセルすべてここを通します。- 状態が
.notDeterminedの場合はrequestAuthorizationを呼んで能動的にダイアログを表示できます。あるいは初回schedule時にシステムに自動で表示させることもできます。 .denied状態のときは UI 上で「アラームは鳴りません」と明示的にユーザーに伝えてください。設定したから安心、と勘違いさせてはいけません。
カウントダウン時間は Alarm.CountdownDuration で表現し、preAlert と postAlert の 2 段階に分かれます(04:08)。
let countdownDuration = Alarm.CountdownDuration(preAlert: (10 * 60), postAlert: (5 * 60))
ポイント:
preAlertは最初に鳴るまでのカウントダウンで、単位は秒です。10 分でステーキを焼くなら10 * 60と書きます。postAlertはユーザーが「繰り返し」または「スヌーズ」を押した後、再度鳴るまでのカウントダウンです。- 両方ともオプションで、純粋な定刻アラーム(カウントダウンを伴わないもの)では省略可能です。
スケジューリングポリシーは 2 種類あります。固定スケジュール Alarm.Schedule.fixed(_:) は絶対的な Date を受け取り、タイムゾーン変化の影響を受けません(04:40)。
let keynoteDateComponents = DateComponents(
calendar: .current,
year: 2025, month: 6, day: 9,
hour: 9, minute: 41)
let keynoteDate = Calendar.current.date(from: keynoteDateComponents)!
let scheduleFixed = Alarm.Schedule.fixed(keynoteDate)
相対スケジュール Alarm.Schedule.Relative は時刻と繰り返しルールを受け取り、デバイスのタイムゾーンに追従します(05:13)。
let time = Alarm.Schedule.Relative.Time(hour: 7, minute: 0)
let recurrence = Alarm.Schedule.Relative.Recurrence.weekly([
.monday, .wednesday, .friday
])
let schedule = Alarm.Schedule.Relative(time: time, repeats: recurrence)
ポイント:
- 出張用のアラームには
fixedを使います。東京に飛んでも、出発地の絶対時刻で鳴ります。 - 平日の早起きアラームには
Relativeを使います。タイムゾーンをまたぐと自動で現地の朝 7 時に調整されます。 Recurrence.weekly([.monday, ...])はSetで曜日の繰り返しを表現し、直感的です。
外観の設定は AlarmAttributes と AlarmConfiguration の 2 層構造を中心にしています。最小構成、つまり「ダイアログを表示するだけで解除ボタンが 1 つだけ」の設定は次のとおりです(05:43)。
func scheduleAlarm() async throws {
typealias AlarmConfiguration = AlarmManager.AlarmConfiguration<CookingData>
let id = UUID()
let duration = Alarm.CountdownDuration(preAlert: (10 * 60), postAlert: (5 * 60))
let stopButton = AlarmButton(
text: "Dismiss",
textColor: .white,
systemImageName: "stop.circle")
let alertPresentation = AlarmPresentation.Alert(
title: "Food Ready!",
stopButton: stopButton)
let attributes = AlarmAttributes<CookingData>(
presentation: AlarmPresentation(alert: alertPresentation),
tintColor: Color.green)
let alarmConfiguration = AlarmConfiguration(
countdownDuration: duration,
attributes: attributes)
try await AlarmManager.shared.schedule(id: id, configuration: alarmConfiguration)
}
ポイント:
id: UUIDは App 側で生成して保存します。後からアラームをキャンセルしたりクエリしたりするときに使うのは、この id です。AlarmButtonの 3 点セットは、テキスト、テキストカラー、SF Symbol 名です。SF Symbol は Dynamic Island のコンパクトモードで使われます。AlarmAttributes<CookingData>のジェネリックパラメータはカスタムメタデータの型です。Neverを渡しても構いませんが、メタデータを持たせたほうが柔軟になります。tintColorはシステム外観でのカウントダウンのメインカラーを制御します。
「繰り返し」ボタンを押して再度カウントダウンを開始したい場合は、secondaryButton を追加し secondaryButtonBehavior を .countdown に設定します(07:17)。ボタンを押すと App 内に飛んでカスタムロジックを実行したい場合は、挙動を .custom に設定し、LiveActivityIntent を提供します(14:09)。
let secondaryIntent = OpenInApp(alarmID: id.uuidString)
let openButton = AlarmButton(
text: "Open",
textColor: .white,
systemImageName: "arrow.right.circle.fill")
let alertPresentation = AlarmPresentation.Alert(
title: "Food Ready!",
stopButton: stopButton,
secondaryButton: openButton,
secondaryButtonBehavior: .custom)
// ...
let alarmConfiguration = AlarmConfiguration(
countdownDuration: duration,
attributes: attributes,
secondaryIntent: secondaryIntent)
public struct OpenInApp: LiveActivityIntent {
public func perform() async throws -> some IntentResult { .result() }
public static var title: LocalizedStringResource = "Open App"
public static var openAppWhenRun = true
@Parameter(title: "alarmID")
public var alarmID: String
public init(alarmID: String) { self.alarmID = alarmID }
public init() { self.alarmID = "" }
}
ポイント:
secondaryButtonBehavior: .customとsecondaryIntentを組み合わせて初めて、ロック画面から直接 App のロジックを実行できます。LiveActivityIntentには引数なしのinit()が必須です。システムが逆シリアライズの際に使用します。openAppWhenRun = trueを指定すると、Intent の実行後にボタンが App をフォアグラウンドに引き上げます。alarmIDは@Parameter経由で渡し、どのアラームがトリガーされたのかを Intent に伝えます。
カウントダウン自体のレンダリングには Live Activity が必要です。ActivityConfiguration のジェネリックパラメータは AlarmAttributes<CookingData> を指し、context.state.mode に応じて .countdown / .paused / .alert の 3 つの状態をレンダリングします(09:15)。
struct AlarmLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: AlarmAttributes<CookingData>.self) { context in
switch context.state.mode {
case .countdown:
countdownView(context)
case .paused:
pausedView(context)
case .alert:
alertView(context)
}
} dynamicIsland: { context in
DynamicIsland {
DynamicIslandExpandedRegion(.leading) { leadingView(context) }
DynamicIslandExpandedRegion(.trailing) { trailingView(context) }
} compactLeading: { compactLeadingView(context) }
compactTrailing: { compactTrailingView(context) }
minimal: { minimalView(context) }
}
}
}
ポイント:
- AlarmKit の Live Activity は App の Widget Extension に追加します。書き方は通常の ActivityKit と同じです。
context.state.modeはシステムが注入するステートマシンで、自分で管理する必要はありません。- Dynamic Island のコンパクト、ミニマル、展開の 3 サイズすべてに View を用意してください。そうしないとシステム側で空白が表示されます。
- カスタムメタデータは
context.attributes.metadataから取得でき、SF Symbol の切り替え(焼く/グリルでアイコンを変えるなど)にも使えます。
中核となる気づき
-
何をするか:料理・ベイキング系の App に AlarmKit のカウントダウンを組み込み、
UNUserNotificationCenterのローカル通知を置き換える。- なぜ価値があるか:ローカル通知はサイレントモードや Focus に阻まれて鳴らなくなりますが、AlarmKit はサイレントを貫通します。ステーキを焼きすぎてしまうのは、App がフキダシを表示する程度の話よりはるかに深刻です。
- どう始めるか:Info.plist に
NSAlarmKitUsageDescriptionを追加し、文言は「料理のタイマーに使用します」のように書きます。スケジュール時にpreAlertをレシピのステップ時間にし、postAlertには 1〜2 分の保険を残しておきましょう。
-
何をするか:タイムゾーンをまたぐ出張系 App で、搭乗アラームに
Alarm.Schedule.fixedを使う。- なぜ価値があるか:相対スケジュールはタイムゾーンをまたぐとずれてしまいますが、搭乗時刻は絶対時刻なのでずれてはいけません。固定スケジュールでこそ、「東京に飛んでも出発地で書いた時刻に鳴る」ことを保証できます。
- どう始めるか:搭乗時刻はフライト所在地のタイムゾーンで
DateComponentsを構築し、Dateに変換してからAlarm.Schedule.fixed(_:)に渡します。アラームのidを旅程データベースに保存しておくと、変更時にcancel+ 再scheduleがしやすくなります。
-
何をするか:ランニング/ポモドーロ系 App で
secondaryButtonBehavior: .customを使い、ロック画面のボタンから直接次のステップを実行できるようにする。- なぜ価値があるか:ランニング中のユーザーは手が濡れていてロック解除が大変です。ロック画面のボタンから次のセットの練習に入れたほうが、「App を開いてボタンを探す」よりずっとスムーズです。
- どう始めるか:
LiveActivityIntentを定義し、alarmIDを@Parameterとして渡します。perform()の中でトレーニングデータベースから次のセットのペースを読み出し、新しいアラームを再スケジュールします。
-
何をするか:起床アラーム系 App で、相対スケジュールと
Recurrence.weeklyを組み合わせ、月〜金まで一括設定する。- なぜ価値があるか:ユーザーが最も嫌うのは毎日手動でアラームを調整することです。一度設定すれば週次で繰り返され、タイムゾーンをまたぐ出張でも自動で現地の朝 7 時に鳴ります。
- どう始めるか:
Alarm.Schedule.Relative.Time(hour:minute:)で時分を指定し、.weekly([.monday, .tuesday, ...])で平日を列挙します。週末のルールは別のアラームとして立ち上げましょう。
関連 Session
- Meet ActivityKit — Live Activity フレームワーク入門。AlarmKit のカウントダウン UI はこれを直接利用しています。
- Bring widgets to new places — Widget と Live Activity が iOS 26 で新たに表示される場所について。
- Explore enhancements to App Intents —
LiveActivityIntentは App Intents ファミリーの一員。カスタムボタンの挙動はこれに支えられています。 - What’s new in widgets — ロック画面、Dynamic Island、StandBy の Widget 更新。アラームの外観に関わります。
コメント
GitHub Issues · utterances