AlarmKit アプリを最速級で出荷して 分かったこと — ドキュメントに書いていない7つ —
iOS 26 で AlarmKit が追加されました。サードパーティのアプリが、純正「時計」と同格のアラーム(全画面表示、マナーモード・集中モードでも鳴る、止めるまで鳴り続ける)を鳴らせる初めての公開 API です。私たちはこれを使って、カレンダーの予定を本物のアラームに変える小さなアプリ キーワードアラーム / KeyAlarm を作り、API 公開の数週間後の 2026年8月27日に App Store で公開しました。
AlarmKit を本番で使った記録はまだほとんどありません。先に知っておきたかったことをまとめます。
1. AlarmKit の entitlement は存在しない
これで丸1日を失いました。スケジューラを書いている途中で、entitlements に com.apple.developer.alarmkit: true が入っていました。シミュレータでは普通に動きます。ところが最初の実機ビルドが「プロビジョニングプロファイルに com.apple.developer.alarmkit entitlement が含まれていない」で落ち、Developer Portal を見ても AlarmKit の capability はどこにもありません。
この entitlement は実在しません。Info.plist の用途説明キーと capability を混同した LLM が作り出したものでした。Apple Developer Forums のスレッド 797950 で Apple のエンジニアが、まさにこのパターン(LLM が存在しない entitlement を生成し、開発者が「宣言する entitlement」「申請が要る entitlement」「Info.plist のキー」を区別できなくなっている)が増えていると書いています。
AlarmKit に本当に必要なのは次の2つだけです。
- Info.plist の
NSAlarmKitUsageDescription(注意:xcodebuild -exportLocalizationsはこのキーを拾わないので、各言語のInfoPlist.stringsを手で書く) - 実行時の
AlarmManager.shared.requestAuthorization()
偽の entitlement を消せば実機ビルドは通ります。
2. アラームの ID は決定的でなければならない
AlarmKit のアラームには自分で決める UUID を付けます。私たちはこれを「カレンダー予定の識別子」と「発火時刻を分に丸めたもの」の2つだけから作っています。
// PlannedAlarm.deterministicID — UUIDv5 相当。EventKit が返す開始時刻の
// ミリ秒のぶれで ID が変わらないよう、秒未満は捨てる。
static func deterministicID(eventID: String, fireDate: Date) -> UUID {
let fireMinute = Int(fireDate.timeIntervalSince1970.rounded(.down)) / 60
return uuidV5(name: "\(eventID)|\(fireMinute)")
}
他のものは一切混ぜません。マッチしたルール、予定のタイトル、リード時間の分数も入れません。理由は再スキャン時の挙動です。ID が変わると古いアラームを取り消して新しく登録することになり、その取消と登録の隙間で発火時刻を跨ぐと、アラームは静かに消えます。ID が安定していれば、再スキャンは冪等な reconcile になります。あるべき集合を計算し、AlarmManager.shared.alarms と差分を取り、足りないものを追加し、無くなったものを取り消し、それ以外には触らない。
音・スヌーズ・カウントダウンも意図的に ID には入れず、別の「設定の指紋」で比較して再登録の要否を決めています。ここに落とし穴が1つ。カウントダウンの値は発火が近づくほど自然に縮むので、指紋は「カウントダウンの有無」だけを見て、長さは見ません。値まで比較すると、カウントダウン表示中の同期のたびに「設定が変わった」と誤認し、Live Activity が出ている最中にアラームを取消・再作成してしまいました。
もう1つのルール: 発火済みのアラームは取り消さない。アラームが鳴っている最中にアプリを再起動すると起動時同期が走り、鳴っているアラームを「reconcile で消す」バグを踏みました。今は発火時刻が過去のアラームの取消をスキップしています。
これらは2本のテストで守っていて、絶対に緩めません。1本はルール編集やタイトル変更で ID が変わらないこと、もう1本は同期を2回走らせたとき2回目の AlarmKit 呼び出しがゼロであること。
3. バックグラウンド更新に依存しない。14日先まで先に登録する
BGAppRefreshTask はベストエフォートです。低電力モードのままの端末では何日も走らないことがあります。アラームをバックグラウンドジョブで作る設計だと、失敗の形は「何も鳴らず、誰も気づかない」になります。
だから依存しません。アプリを開いたとき、カレンダーが変わったとき、ルールを編集したとき、その場で14日先までのアラームを全部 AlarmKit に登録します。登録さえ済めば、以後アプリを一度も開かなくても鳴ります。バックグラウンド更新はおまけであって前提ではありません。
代償は同時登録数です。AlarmKit の上限は非公開なので、既定30件・直近優先で打ち切り、切り捨てた件数を黙って落とさずユーザーに見せています。
4. プランナーを純粋に保つ
考える部分は3ファイルだけです: KeywordMatcher、AlarmPlanner、Reconciler。どれも EventKit も AlarmKit も import しません。OS との境界は EventProviding と AlarmScheduling の2つのプロトコルで、テスト用のフェイクがあります。2と3の内容は「2回目は何もしない」性質も含めて全部このフェイクに対する単体テストです。EKEvent や AlarmManager をプランナーに漏らしていたら、実機なしでは何もテストできませんでした。
5. Live Activity のメタデータは共有ターゲットに置く
AlarmKit はカウントダウンを Live Activity として表示し、その型は自分で定義する AlarmAttributes<Metadata> です。この Metadata 型はアプリ本体と Widget Extension で完全に同一でなければなりません。片方のターゲットにしかファイルが無いと、Live Activity は復号に失敗し、エラーも出ずにカウントダウンが空白になります。私たちは KeyAlarmMetadata.swift を Shared/ に置き、両ターゲットの sources に入れています。
6. リモートの kill switch は必ず fail open
サイトに小さな version.json を置き、アラームが鳴らなくなるような不具合が出たときに強制アップデートできるようにしています。この確認は、通信に失敗したら必ず .ok を返します。fail closed にすると、機内モード、キャプティブポータルの Wi-Fi、CDN の障害のどれでも、アラームアプリが文鎮になります。fail open の経路にはテストがあり、これは省略不可です。
7. .timeSensitive は entitlement が無いと黙って格下げされる
AlarmKit とは別の話ですが同じアプリの中で: スキャンの結果、今後のアラームがゼロ件のときは普通の UNNotification を送っています。interruptionLevel = .timeSensitive を Time Sensitive Notifications の entitlement 無しで設定してもエラーにはならず、静かに .active になります。こちらは本物の entitlement で、AlarmKit の混乱が起きやすい理由の一つでもあります。
8. 審査: ガイドライン 5.1.1(iv)
システムの許可ダイアログの前に、なぜカレンダーと AlarmKit が要るかを説明する事前画面を置いています。最初の提出は 5.1.1(iv) でリジェクトされました。理由は、その画面のボタンが「許可する」だったこと。独自画面が許可そのものを与えているように見えてはいけません。ボタンは中立でなければならず、「続ける」に変えただけで通りました。最初から正しくしておけば安いところです。
これを全部使って KeyAlarm がやること
キーワードを一度登録します(「ピアノ」)。タイトルにそれを含むカレンダー予定は、1つ以上のリード時間(準備に30分前、出発に5分前)で本物のアラームになります。サーバーなし、アカウントなし、App Store のプライバシー表示は「データの収集なし」。iOS 26 以降、30日試用のあと ¥100 の買い切りです。
- App Store: apps.apple.com/jp/app/id6801792687
- 製品ページ: ltng.jp/apps/keyalarm
- 英語版の記事: ltng.jp/blog/alarmkit-in-practice
AlarmKit についての質問はお問い合わせフォームからどうぞ。