- Published on
- 6 min read
> Scheduling Alarms with AlarmKit
For years, developers have worked around iOS's lack of alarm APIs by using local notifications or background audio tricks. AlarmKit changes that by letting your app schedule real alarms and countdown timers that appear on the Lock Screen, in the Dynamic Island, and use the system's alarm infrastructure.
The framework centers around AlarmManager, which handles authorization, scheduling, and managing alarms. You configure alarms using AlarmConfiguration and customize their appearance with AlarmPresentation.
Setup Requirements
Before using AlarmKit, add the NSAlarmKitUsageDescription key to your Info.plist with a description explaining why your app needs to schedule alarms.
Requesting Authorization
AlarmKit uses a familiar authorization pattern. Check the current state before prompting:
import AlarmKit
func checkAuthorization() async -> Bool {
let manager = AlarmManager.shared
switch manager.authorizationState {
case .notDetermined:
do {
let state = try await manager.requestAuthorization()
return state == .authorized
} catch {
print("Authorization failed: \(error)")
return false
}
case .authorized:
return true
case .denied:
return false
@unknown default:
return false
}
}
The system shows a permission prompt explaining that your app wants to create alarms. Once granted, this permission persists until the user revokes it in Settings.
Creating Alarm Metadata
AlarmKit uses a type-safe metadata system to pass contextual data between your alarm scheduling code and the Live Activity presentation. Define a struct conforming to AlarmMetadata:
import AlarmKit
struct MorningAlarmData: AlarmMetadata {
var label: String
}
This metadata gets associated with your alarm and can be accessed when the alarm fires.
Configuring Alarm Presentation
Alarms need presentation configuration that defines how they appear to users. Create an alert with buttons for the user to interact with:
import AlarmKit
import SwiftUI
func createAlarmPresentation() -> AlarmPresentation {
let stopButton = AlarmButton(
text: "Dismiss",
textColor: .white,
systemImageName: "stop.circle"
)
let snoozeButton = AlarmButton(
text: "Snooze",
textColor: .white,
systemImageName: "clock"
)
let alert = AlarmPresentation.Alert(
title: "Wake Up",
stopButton: stopButton,
secondaryButton: snoozeButton,
secondaryButtonBehavior: .snooze
)
return AlarmPresentation(alert: alert)
}
Scheduling a Countdown Timer
The simplest alarm type is a countdown timer. Create an AlarmConfiguration with a duration:
import AlarmKit
import SwiftUI
func scheduleTimer(duration: TimeInterval) async throws {
let presentation = createAlarmPresentation()
let attributes = AlarmAttributes<MorningAlarmData>(
presentation: presentation,
tintColor: .blue
)
let configuration = AlarmConfiguration.timer(
duration: duration,
attributes: attributes
)
try await AlarmManager.shared.schedule(
id: UUID(),
configuration: configuration
)
}
Scheduling a Fixed-Time Alarm
For alarms at a specific date and time, use Alarm.Schedule.fixed:
import AlarmKit
import SwiftUI
func scheduleAlarmAt(date: Date) async throws {
let presentation = createAlarmPresentation()
let attributes = AlarmAttributes<MorningAlarmData>(
presentation: presentation,
tintColor: .orange
)
let configuration = AlarmConfiguration(
schedule: .fixed(date),
attributes: attributes
)
try await AlarmManager.shared.schedule(
id: UUID(),
configuration: configuration
)
}
Recurring Alarms
For alarms that repeat on specific days, use Alarm.Schedule.Relative with a recurrence pattern:
import AlarmKit
import SwiftUI
func scheduleWeekdayAlarm() async throws {
let presentation = createAlarmPresentation()
let attributes = AlarmAttributes<MorningAlarmData>(
presentation: presentation,
tintColor: .green
)
let time = Alarm.Schedule.Relative.Time(hour: 7, minute: 30)
let recurrence = Alarm.Schedule.Relative.Recurrence.weekly([
.monday, .tuesday, .wednesday, .thursday, .friday
])
let schedule = Alarm.Schedule.relative(
Alarm.Schedule.Relative(time: time, repeats: recurrence)
)
let configuration = AlarmConfiguration(
schedule: schedule,
attributes: attributes
)
try await AlarmManager.shared.schedule(
id: UUID(),
configuration: configuration
)
}
Managing Scheduled Alarms
Access your scheduled alarms through the alarms property and stop them with stop(id:):
import AlarmKit
func listMyAlarms() -> [Alarm] {
return AlarmManager.shared.alarms
}
func stopAlarm(id: UUID) async throws {
try await AlarmManager.shared.stop(id: id)
}
Note that you can only access alarms your app created. Alarms from other apps aren't visible to your app.
Handling Custom Actions
To run code when users tap alarm buttons, define intents conforming to LiveActivityIntent:
import AppIntents
import AlarmKit
struct DismissAlarmIntent: LiveActivityIntent {
static var title: LocalizedStringResource = "Dismiss Alarm"
@Parameter(title: "Alarm ID")
var alarmID: String
func perform() async throws -> some IntentResult {
// Handle dismissal logic here
return .result()
}
}
Then associate the intent with a button using secondaryIntent in your AlarmConfiguration.
Practical Example
Here's a view that lets users schedule a quick countdown timer:
import SwiftUI
import AlarmKit
struct MorningAlarmData: AlarmMetadata {
var label: String
}
struct TimerSchedulerView: View {
@State private var duration: TimeInterval = 300
@State private var isAuthorized = false
private let manager = AlarmManager.shared
var body: some View {
Form {
Section("Timer Duration") {
Picker("Duration", selection: $duration) {
Text("1 minute").tag(TimeInterval(60))
Text("5 minutes").tag(TimeInterval(300))
Text("10 minutes").tag(TimeInterval(600))
Text("30 minutes").tag(TimeInterval(1800))
}
}
Section {
Button("Start Timer") {
Task { await scheduleTimer() }
}
.disabled(!isAuthorized)
}
Section("Active Alarms") {
ForEach(manager.alarms, id: \.id) { alarm in
HStack {
Text("Timer")
Spacer()
Button("Stop") {
Task {
try? await manager.stop(id: alarm.id)
}
}
}
}
}
}
.task {
isAuthorized = await checkAuthorization()
}
}
func checkAuthorization() async -> Bool {
switch manager.authorizationState {
case .notDetermined:
do {
let state = try await manager.requestAuthorization()
return state == .authorized
} catch {
return false
}
case .authorized:
return true
default:
return false
}
}
func scheduleTimer() async {
let stopButton = AlarmButton(
text: "Done",
textColor: .white,
systemImageName: "checkmark"
)
let alert = AlarmPresentation.Alert(
title: "Timer Complete",
stopButton: stopButton
)
let attributes = AlarmAttributes<MorningAlarmData>(
presentation: AlarmPresentation(alert: alert),
tintColor: .blue
)
let configuration = AlarmConfiguration.timer(
duration: duration,
attributes: attributes
)
do {
try await manager.schedule(
id: UUID(),
configuration: configuration
)
} catch {
print("Failed to schedule: \(error)")
}
}
}
Entitlement Requirements
AlarmKit requires a special entitlement that you need to request from Apple. Add the AlarmKit capability in your app's Signing & Capabilities, then apply for access through the Apple Developer portal. Apple reviews these requests to ensure apps have a legitimate use case for alarm functionality.
Without the entitlement, AlarmKit APIs will throw authorization errors even if the user grants permission.
Things to Know
Alarms scheduled through AlarmKit respect the user's Do Not Disturb and Focus settings. The system handles sound playback and the alarm UI on the Lock Screen and Dynamic Island.
AlarmKit also requires implementing a Live Activity for the countdown interface. The AlarmPresentation you configure defines how your alarm appears in the system's alarm UI.
For complete API details, see Apple's AlarmKit documentation.
Sample Project
Want to see this code in action? Check out the complete sample project on GitHub:
The repository includes a working Xcode project with all the examples from this article, plus unit tests you can run to verify the behavior.
// Continue_Learning
The Difference Between Frame and Bounds in UIKit
Understand the key difference between a UIView's frame and bounds properties, and when to use each in your iOS apps.
TaskGroup and Structured Concurrency Patterns in Swift
Master Swift's TaskGroup for parallel async work with proper cancellation, error handling, and result collection. Learn common patterns for batch operations and concurrent pipelines.
Typed Throws in Swift 6: Declaring Specific Error Types
Swift 6 introduces typed throws, letting you declare exactly which error types a function can throw. Learn how this improves API contracts and enables exhaustive error handling.
// Stay Updated
Get notified when I publish new tutorials on Swift, SwiftUI, and iOS development. No spam, unsubscribe anytime.