WWDC Quick Look 💓 By SwiftGGTeam
Wake up to the AlarmKit API

Wake up to the AlarmKit API

元の動画を見る

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 で表現し、preAlertpostAlert の 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 で曜日の繰り返しを表現し、直感的です。

外観の設定は AlarmAttributesAlarmConfiguration の 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: .customsecondaryIntent を組み合わせて初めて、ロック画面から直接 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 IntentsLiveActivityIntent は App Intents ファミリーの一員。カスタムボタンの挙動はこれに支えられています。
  • What’s new in widgets — ロック画面、Dynamic Island、StandBy の Widget 更新。アラームの外観に関わります。

コメント

GitHub Issues · utterances