Add scan flow MVP and local Axiom skill workspace
This snapshot establishes the camera-to-result recognition flow and related tests while checking in the project skill/docs assets required for the configured local tooling.
This commit is contained in:
502
.claude/skills/axiom-alarmkit-ref/SKILL.md
Normal file
502
.claude/skills/axiom-alarmkit-ref/SKILL.md
Normal file
@@ -0,0 +1,502 @@
|
||||
---
|
||||
name: axiom-alarmkit-ref
|
||||
description: Use when implementing alarm functionality, scheduling wake alarms, or integrating AlarmKit with Live Activities. Covers AlarmKit authorization, alarm configuration, SwiftUI views, and Live Activity integration.
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# AlarmKit Reference
|
||||
|
||||
Complete API reference for AlarmKit, Apple's framework for scheduling alarms and countdown timers with system-level alerting, Dynamic Island integration, and focus/silent mode override.
|
||||
|
||||
## Overview
|
||||
|
||||
AlarmKit lets apps create alarms and timers that behave like the built-in Clock app -- they override Do Not Disturb, appear in the Dynamic Island, and show on the Lock Screen. The framework handles scheduling, snooze, pause/resume, and UI presentation through a small set of types centered on `AlarmManager`.
|
||||
|
||||
## System Requirements
|
||||
|
||||
- **iOS 26+** (AlarmKit introduced in iOS 26)
|
||||
- **Widget Extension** required for Live Activity / Dynamic Island presentation
|
||||
- **Physical device** recommended for alarm sound and notification testing
|
||||
|
||||
---
|
||||
|
||||
## Part 1: Key Components
|
||||
|
||||
### AlarmManager
|
||||
|
||||
Singleton entry point for all alarm operations.
|
||||
|
||||
```swift
|
||||
import AlarmKit
|
||||
|
||||
let manager = AlarmManager.shared
|
||||
```
|
||||
|
||||
All scheduling, cancellation, and observation flows through this shared instance.
|
||||
|
||||
### Alarm
|
||||
|
||||
Describes an alarm that can alert once or on a repeating schedule.
|
||||
|
||||
```swift
|
||||
struct Alarm {
|
||||
var id: UUID
|
||||
var schedule: Schedule?
|
||||
var countdownDuration: CountdownDuration?
|
||||
var state: AlarmState
|
||||
}
|
||||
```
|
||||
|
||||
### AlarmPresentation
|
||||
|
||||
Content for the alarm UI across three states -- alerting, counting down, and paused.
|
||||
|
||||
```swift
|
||||
struct AlarmPresentation {
|
||||
var alert: Alert // Required: shown when alarm fires
|
||||
var countdown: Countdown? // Optional: shown during countdown
|
||||
var paused: Paused? // Optional: shown when paused
|
||||
}
|
||||
```
|
||||
|
||||
### AlarmAttributes
|
||||
|
||||
Generic container pairing presentation with app-specific metadata and tint color. Used to configure the Live Activity widget.
|
||||
|
||||
```swift
|
||||
struct AlarmAttributes<Metadata: AlarmMetadata> {
|
||||
var presentation: AlarmPresentation
|
||||
var metadata: Metadata
|
||||
var tintColor: Color
|
||||
}
|
||||
```
|
||||
|
||||
### AlarmMetadata
|
||||
|
||||
Protocol for app-specific data attached to an alarm. Conform an empty struct for minimal usage, or add properties for richer UI.
|
||||
|
||||
```swift
|
||||
struct RecipeMetadata: AlarmMetadata {
|
||||
let recipeName: String
|
||||
let cookingStep: String
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Part 2: Authorization
|
||||
|
||||
Apps must request permission before scheduling alarms. Add `NSAlarmKitUsageDescription` to Info.plist.
|
||||
|
||||
### Requesting Authorization
|
||||
|
||||
```swift
|
||||
func requestAlarmAuthorization() async -> Bool {
|
||||
do {
|
||||
let state = try await AlarmManager.shared.requestAuthorization()
|
||||
return state == .authorized
|
||||
} catch {
|
||||
print("Authorization error: \(error)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Checking Current State
|
||||
|
||||
Use `authorizationState` (not `authorizationStatus`) to read the current value:
|
||||
|
||||
```swift
|
||||
let state = await AlarmManager.shared.authorizationState
|
||||
// .authorized | .denied | .notDetermined
|
||||
```
|
||||
|
||||
### Observing Authorization Changes
|
||||
|
||||
```swift
|
||||
for await authState in AlarmManager.shared.authorizationUpdates {
|
||||
switch authState {
|
||||
case .authorized: enableAlarmUI()
|
||||
case .denied: showPermissionPrompt()
|
||||
case .notDetermined: break
|
||||
@unknown default: break
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Part 3: Scheduling Alarms
|
||||
|
||||
Every alarm requires a `UUID`, an `AlarmManager.AlarmConfiguration`, and a call to `schedule(id:configuration:)`.
|
||||
|
||||
### One-Time Alarm
|
||||
|
||||
```swift
|
||||
let id = UUID()
|
||||
let time = Alarm.Schedule.Relative.Time(hour: 7, minute: 30)
|
||||
let schedule = Alarm.Schedule.relative(.init(
|
||||
time: time,
|
||||
repeats: .never
|
||||
))
|
||||
|
||||
let alert = AlarmPresentation.Alert(
|
||||
title: "Wake Up",
|
||||
stopButton: .stopButton,
|
||||
secondaryButton: .snoozeButton,
|
||||
secondaryButtonBehavior: .countdown
|
||||
)
|
||||
|
||||
struct EmptyMetadata: AlarmMetadata {}
|
||||
let config = AlarmManager.AlarmConfiguration(
|
||||
countdownDuration: nil,
|
||||
schedule: schedule,
|
||||
attributes: AlarmAttributes(
|
||||
presentation: AlarmPresentation(alert: alert),
|
||||
metadata: EmptyMetadata(),
|
||||
tintColor: .blue
|
||||
),
|
||||
sound: .default
|
||||
)
|
||||
|
||||
let alarm = try await AlarmManager.shared.schedule(id: id, configuration: config)
|
||||
```
|
||||
|
||||
### Repeating Alarm
|
||||
|
||||
Use `.weekly(Array(weekdays))` for specific days:
|
||||
|
||||
```swift
|
||||
let time = Alarm.Schedule.Relative.Time(hour: 6, minute: 0)
|
||||
let schedule = Alarm.Schedule.relative(.init(
|
||||
time: time,
|
||||
repeats: .weekly([.monday, .tuesday, .wednesday, .thursday, .friday])
|
||||
))
|
||||
```
|
||||
|
||||
### Countdown Timer
|
||||
|
||||
Set `schedule: nil` and provide `countdownDuration` with a `preAlert` interval:
|
||||
|
||||
```swift
|
||||
let countdown = Alarm.CountdownDuration(
|
||||
preAlert: 300, // 5 minutes
|
||||
postAlert: 10 // Optional post-alert snooze window
|
||||
)
|
||||
|
||||
let config = AlarmManager.AlarmConfiguration(
|
||||
countdownDuration: countdown,
|
||||
schedule: nil,
|
||||
attributes: attributes,
|
||||
sound: .default
|
||||
)
|
||||
```
|
||||
|
||||
Timers support pause/resume and show a countdown presentation when `AlarmPresentation.countdown` is provided.
|
||||
|
||||
### Snooze Configuration
|
||||
|
||||
Snooze uses `CountdownDuration.postAlert` combined with a `.snoozeButton` secondary action:
|
||||
|
||||
```swift
|
||||
let alert = AlarmPresentation.Alert(
|
||||
title: "Alarm",
|
||||
stopButton: .stopButton,
|
||||
secondaryButton: .snoozeButton,
|
||||
secondaryButtonBehavior: .countdown // Starts post-alert countdown
|
||||
)
|
||||
|
||||
let countdownDuration = Alarm.CountdownDuration(
|
||||
preAlert: nil,
|
||||
postAlert: 9 * 60 // 9-minute snooze
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Part 4: Customizing Alarm UI
|
||||
|
||||
### Alert Presentation
|
||||
|
||||
The alert state is shown when the alarm fires. The stop button is required; secondary button is optional.
|
||||
|
||||
```swift
|
||||
// Minimal
|
||||
let basic = AlarmPresentation.Alert(
|
||||
title: "Alarm",
|
||||
stopButton: .stopButton
|
||||
)
|
||||
|
||||
// With custom button labels
|
||||
let custom = AlarmPresentation.Alert(
|
||||
title: "Medication Reminder",
|
||||
stopButton: AlarmButton(label: "Taken"),
|
||||
secondaryButton: AlarmButton(label: "Remind Later"),
|
||||
secondaryButtonBehavior: .countdown
|
||||
)
|
||||
|
||||
// With open-app action
|
||||
let openApp = AlarmPresentation.Alert(
|
||||
title: "Workout Time",
|
||||
stopButton: .stopButton,
|
||||
secondaryButton: .openAppButton,
|
||||
secondaryButtonBehavior: .custom
|
||||
)
|
||||
```
|
||||
|
||||
### Countdown Presentation
|
||||
|
||||
Shown while a timer counts down. Only relevant for alarms with `countdownDuration.preAlert`.
|
||||
|
||||
```swift
|
||||
let countdown = AlarmPresentation.Countdown(
|
||||
title: "Timer Running",
|
||||
pauseButton: .pauseButton
|
||||
)
|
||||
```
|
||||
|
||||
### Paused Presentation
|
||||
|
||||
Shown when a countdown timer is paused.
|
||||
|
||||
```swift
|
||||
let paused = AlarmPresentation.Paused(
|
||||
title: "Timer Paused",
|
||||
resumeButton: .resumeButton
|
||||
)
|
||||
```
|
||||
|
||||
### Full Three-State Presentation
|
||||
|
||||
Combine all three for a complete timer experience:
|
||||
|
||||
```swift
|
||||
let presentation = AlarmPresentation(
|
||||
alert: AlarmPresentation.Alert(
|
||||
title: "Timer Complete",
|
||||
stopButton: .stopButton,
|
||||
secondaryButton: .repeatButton,
|
||||
secondaryButtonBehavior: .countdown
|
||||
),
|
||||
countdown: AlarmPresentation.Countdown(
|
||||
title: "Cooking Timer",
|
||||
pauseButton: .pauseButton
|
||||
),
|
||||
paused: AlarmPresentation.Paused(
|
||||
title: "Timer Paused",
|
||||
resumeButton: .resumeButton
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Part 5: Managing Alarms
|
||||
|
||||
### Retrieve All Alarms
|
||||
|
||||
```swift
|
||||
let alarms = try AlarmManager.shared.alarms
|
||||
```
|
||||
|
||||
### Pause / Resume
|
||||
|
||||
```swift
|
||||
try await AlarmManager.shared.pause(id: alarmID)
|
||||
try await AlarmManager.shared.resume(id: alarmID)
|
||||
```
|
||||
|
||||
### Cancel
|
||||
|
||||
```swift
|
||||
try await AlarmManager.shared.cancel(id: alarmID)
|
||||
```
|
||||
|
||||
### Observe Alarm Updates
|
||||
|
||||
Use `alarmUpdates` to keep UI in sync. An alarm absent from the emitted array is no longer scheduled.
|
||||
|
||||
```swift
|
||||
for await alarms in AlarmManager.shared.alarmUpdates {
|
||||
self.alarms = alarms
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Part 6: Live Activity Integration
|
||||
|
||||
AlarmKit alarms appear in the Dynamic Island and Lock Screen through `ActivityConfiguration`. Add a Widget Extension target and implement the widget using `AlarmAttributes`.
|
||||
|
||||
```swift
|
||||
struct AlarmWidgetView: Widget {
|
||||
var body: some WidgetConfiguration {
|
||||
ActivityConfiguration(for: AlarmAttributes<YourMetadata>.self) { context in
|
||||
// Lock Screen presentation
|
||||
VStack {
|
||||
Text(context.attributes.presentation.alert.title)
|
||||
if context.state.mode == .countdown {
|
||||
Text(
|
||||
timerInterval: context.state.countdownEndDate
|
||||
.timeIntervalSinceNow,
|
||||
countsDown: true
|
||||
)
|
||||
.bold()
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
} dynamicIsland: { context in
|
||||
DynamicIsland {
|
||||
DynamicIslandExpandedRegion(.leading) {
|
||||
Text(context.attributes.presentation.alert.title)
|
||||
}
|
||||
DynamicIslandExpandedRegion(.trailing) {
|
||||
if context.state.mode == .countdown {
|
||||
Text(
|
||||
timerInterval: context.state.countdownEndDate
|
||||
.timeIntervalSinceNow,
|
||||
countsDown: true
|
||||
)
|
||||
}
|
||||
}
|
||||
} compactLeading: {
|
||||
Image(systemName: "alarm")
|
||||
} compactTrailing: {
|
||||
if context.state.mode == .countdown {
|
||||
Text(
|
||||
timerInterval: context.state.countdownEndDate
|
||||
.timeIntervalSinceNow,
|
||||
countsDown: true
|
||||
)
|
||||
}
|
||||
} minimal: {
|
||||
Image(systemName: "alarm")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Part 7: SwiftUI Integration
|
||||
|
||||
### ViewModel Pattern with @Observable
|
||||
|
||||
```swift
|
||||
import AlarmKit
|
||||
|
||||
@Observable
|
||||
class AlarmViewModel {
|
||||
var alarms: [Alarm] = []
|
||||
private let manager = AlarmManager.shared
|
||||
|
||||
func requestAuthorization() {
|
||||
Task {
|
||||
_ = try? await manager.requestAuthorization()
|
||||
}
|
||||
}
|
||||
|
||||
func loadAndObserve() {
|
||||
Task {
|
||||
alarms = (try? manager.alarms) ?? []
|
||||
for await updated in manager.alarmUpdates {
|
||||
alarms = updated
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func addAlarm(hour: Int, minute: Int, weekdays: Set<Locale.Weekday>) {
|
||||
Task {
|
||||
let time = Alarm.Schedule.Relative.Time(hour: hour, minute: minute)
|
||||
let schedule = Alarm.Schedule.relative(.init(
|
||||
time: time,
|
||||
repeats: weekdays.isEmpty ? .never : .weekly(Array(weekdays))
|
||||
))
|
||||
|
||||
let alert = AlarmPresentation.Alert(
|
||||
title: "Alarm",
|
||||
stopButton: .stopButton,
|
||||
secondaryButton: .snoozeButton,
|
||||
secondaryButtonBehavior: .countdown
|
||||
)
|
||||
|
||||
struct EmptyMetadata: AlarmMetadata {}
|
||||
let config = AlarmManager.AlarmConfiguration(
|
||||
countdownDuration: Alarm.CountdownDuration(
|
||||
preAlert: nil, postAlert: 9 * 60
|
||||
),
|
||||
schedule: schedule,
|
||||
attributes: AlarmAttributes(
|
||||
presentation: AlarmPresentation(alert: alert),
|
||||
metadata: EmptyMetadata(),
|
||||
tintColor: .blue
|
||||
),
|
||||
sound: .default
|
||||
)
|
||||
|
||||
_ = try? await manager.schedule(id: UUID(), configuration: config)
|
||||
}
|
||||
}
|
||||
|
||||
func cancel(id: UUID) {
|
||||
Task { try? await manager.cancel(id: id) }
|
||||
}
|
||||
|
||||
func togglePause(id: UUID, isPaused: Bool) {
|
||||
Task {
|
||||
if isPaused {
|
||||
try? await manager.resume(id: id)
|
||||
} else {
|
||||
try? await manager.pause(id: id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Alarm List View
|
||||
|
||||
```swift
|
||||
struct AlarmListView: View {
|
||||
@State private var viewModel = AlarmViewModel()
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
List(viewModel.alarms, id: \.id) { alarm in
|
||||
AlarmRow(alarm: alarm, viewModel: viewModel)
|
||||
}
|
||||
.navigationTitle("Alarms")
|
||||
.onAppear {
|
||||
viewModel.requestAuthorization()
|
||||
viewModel.loadAndObserve()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Part 8: Best Practices
|
||||
|
||||
| Practice | Detail |
|
||||
|----------|--------|
|
||||
| Request authorization early | On first launch or first alarm creation attempt |
|
||||
| Handle denial gracefully | Guide users to Settings if permission was denied |
|
||||
| Persist alarm UUIDs | Store IDs to manage alarms across app launches |
|
||||
| Implement widget extension | Required for countdown/Dynamic Island presentation |
|
||||
| Use `alarmUpdates` | Keep UI in sync; don't poll or cache stale state |
|
||||
| Test on physical device | Alarm sounds, notifications, and Live Activities require real hardware |
|
||||
| Respect system limits | There is a system-imposed cap on alarms per app |
|
||||
| Use `authorizationState` | Not `authorizationStatus` -- the correct property name is `authorizationState` |
|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
|
||||
**WWDC**: 2025-230
|
||||
|
||||
**Docs**: /alarmkit, /alarmkit/alarmmanager, /alarmkit/alarm, /alarmkit/alarmpresentation, /alarmkit/alarmattributes
|
||||
|
||||
**Skills**: axiom-extensions-widgets-ref, axiom-swiftui-26-ref
|
||||
Reference in New Issue
Block a user