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

Wake up to the AlarmKit API

观看原视频

Highlight

iOS 26 通过 AlarmKit 把”会破静音、会穿透专注模式”的闹钟能力开放给第三方 App,配合 Live Activity 在锁屏和灵动岛展示倒计时,并允许通过 App Intents 自定义按钮行为。


核心内容

第三方 App 一直被一个边界卡住:本地通知会被静音和专注模式压住,到点了用户根本听不见。做菜计时器、起床闹钟、长跑配速提醒这类需要”必须响”的场景,开发者要么劝用户关静音,要么走推送+音频后台播放的灰色路径。系统级的闹钟能力,过去只有 Apple 自家的”时钟”应用能用。

iOS 26 的 AlarmKit 把这道墙拆了。框架提供和系统闹钟一样的优先级——响铃时穿透静音和当前 Focus,配合锁屏、灵动岛、StandBy 和 Apple Watch 的展示。开发者在 Info.plist 里声明 NSAlarmKitUsageDescription,用户授权之后,App 就可以用 AlarmManager.shared.schedule(id:configuration:) 调度闹钟。一个闹钟由四部分组成:倒计时时长、调度策略(固定时间或相对时间循环)、外观配置(标题、按钮、颜色)和可选的自定义声音。Live Activity 不是装饰,是必需品——只要闹钟带倒计时功能,App 就必须实现对应的 Live Activity,否则系统会用兜底的系统外观。


详细内容

调度一个闹钟之前要先检查授权状态。AlarmManager.shared.authorizationState 返回 .notDetermined / .authorized / .denied 三种值(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 两段(04:08)。

let countdownDuration = Alarm.CountdownDuration(preAlert: (10 * 60), postAlert: (5 * 60))

关键点:

  • preAlert 是首次响铃前的倒计时,单位秒。10 分钟煎牛排,写 10 * 60
  • postAlert 是用户按”重复”或”贪睡”后再次响铃前的倒计时。
  • 两个值都可选,纯定点闹钟(不带倒计时)可以省略。

调度策略有两种。固定调度 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 两层结构。最小化的”只显示弹窗+一个解除按钮”配置如下(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 生成并保存——后续取消、查询闹钟都靠它。
  • AlarmButton 三件套:文字、文字颜色、SF Symbol 名。SF Symbol 用于灵动岛紧凑模式。
  • AlarmAttributes<CookingData> 的泛型参数是自定义元数据类型,传 Never 也行,但带元数据更灵活。
  • tintColor 控制系统外观下倒计时主色调。

如果想要”重复”按钮触发再次倒计时,加 secondaryButton 并把 secondaryButtonBehavior 设成 .countdown07:17)。如果想要按钮跳进 App 里执行自定义逻辑,把行为设成 .custom 并提供一个 LiveActivityIntent14: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 三种状态(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 的紧凑、最小、展开三种尺寸都要给视图,否则系统会显示空白。
  • 自定义元数据通过 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

评论

GitHub Issues · utterances