A comprehensive SwiftUI app demonstrating AlarmKit usage in iOS 26, featuring custom alarm scheduling, snooze functionality, color customization, and SwiftData persistence.
- System-Level Alarms: AlarmKit integration with Lock Screen and Dynamic Island support
- Custom Snooze Duration: Configurable snooze interval (1m, 5m, 10m, 30m, 1h, 2h, 4h, 1d)
- Color Customization: Custom button and text colors for alarm presentations
- Alarm Editing: Full CRUD operations with tap-to-edit functionality
- SwiftData Persistence: Modern data persistence with custom color storage
- Rich UI Components: Modular SwiftUI architecture with reusable components
- iOS 26.0+
- Xcode 26.0+
- Swift 6.0+
-
Add AlarmKit Framework
import AlarmKit -
Configure Info.plist Add the required usage description:
<key>NSAlarmKitUsageDescription</key> <string>This app needs alarm permissions to schedule custom alarms with snooze functionality.</string>
-
Request Authorization
let status = try await AlarmManager.shared.requestAuthorization()
The central coordinator for all alarm operations:
// Request permission
let status = try await AlarmManager.shared.requestAuthorization()
// Schedule an alarm
let alarmID = try await AlarmManager.shared.schedule(
id: UUID(),
configuration: alarmConfiguration
)
// Stop an alarm
try await AlarmManager.shared.stop(id: alarmID)Controls how alarms appear to users with three distinct states:
Alert Presentation (when alarm fires):
let alertPresentation = AlarmPresentation.Alert(
title: LocalizedStringResource(stringLiteral: "Wake Up!"),
stopButton: AlarmButton(
text: "Done",
textColor: .white,
systemImageName: "checkmark.seal.fill"
),
secondaryButton: AlarmButton(
text: "Snooze",
textColor: .white,
systemImageName: "repeat.circle.fill"
),
secondaryButtonBehavior: .countdown
)Countdown Presentation (during snooze):
let countdownPresentation = AlarmPresentation.Countdown(
title: LocalizedStringResource(stringLiteral: "Snoozing - 5 minutes remaining"),
pauseButton: AlarmButton(
text: "Snooze",
textColor: .white,
systemImageName: "repeat.circle.fill"
)
)Complete Presentation:
let presentation = AlarmPresentation(
alert: alertPresentation,
countdown: countdownPresentation
)Defines the complete alarm behavior:
// Countdown duration for snooze functionality
let countdownDuration = Alarm.CountdownDuration(
preAlert: nil, // No pre-alert countdown
postAlert: TimeInterval(300) // 5-minute snooze duration
)
// Alarm schedule
let schedule = Alarm.Schedule.relative(Alarm.Schedule.Relative(
time: Alarm.Schedule.Relative.Time(hour: 7, minute: 30),
repeats: Alarm.Schedule.Relative.Recurrence.weekly([.monday, .tuesday, .wednesday, .thursday, .friday])
))
// Alarm attributes
let attributes = AlarmAttributes(
presentation: presentation,
metadata: CustomMetadata(),
tintColor: .blue
)
// Complete configuration
let alarmConfiguration = AlarmManager.AlarmConfiguration(
countdownDuration: countdownDuration,
schedule: schedule,
attributes: attributes,
secondaryIntent: nil,
sound: .default
)Since SwiftData doesn't natively support Color storage, we use hex strings:
@Model
class AlarmModel {
// Core properties
var name: String
var hour: Int
var minute: Int
var selectedDays: Set<Int>
var snoozeDelay: Int
// Color storage as hex strings
var buttonColorHex: String
var textColorHex: String
// Computed properties for SwiftUI
var buttonColor: Color {
return Color(hex: buttonColorHex) ?? .blue
}
var textColor: Color {
return Color(hex: textColorHex) ?? .white
}
init(/* parameters */) {
// Convert colors to hex for storage
self.buttonColorHex = buttonColor.toHex()
self.textColorHex = textColor.toHex()
}
}extension Color {
func toHex() -> String {
let uiColor = UIColor(self)
var red: CGFloat = 0, green: CGFloat = 0, blue: CGFloat = 0
uiColor.getRed(&red, green: &green, blue: &blue, alpha: nil)
let rgb: Int = (Int)(red * 255) << 16 | (Int)(green * 255) << 8 | (Int)(blue * 255)
return String(format: "#%06x", rgb)
}
init?(hex: String) {
guard hex.hasPrefix("#"), hex.count == 7 else { return nil }
let scanner = Scanner(string: String(hex.dropFirst()))
var hexNumber: UInt64 = 0
guard scanner.scanHexInt64(&hexNumber) else { return nil }
self.init(
red: CGFloat((hexNumber & 0xff0000) >> 16) / 255,
green: CGFloat((hexNumber & 0x00ff00) >> 8) / 255,
blue: CGFloat(hexNumber & 0x0000ff) / 255
)
}
}@MainActor
private func saveAlarm() async {
do {
// 1. Request authorization
try await requestAlarmAuthorization()
// 2. Create or update alarm model
let alarm = createAlarmModel()
modelContext.insert(alarm)
try modelContext.save()
// 3. Schedule with AlarmKit
let weekdays = convertToAlarmKitWeekdays(selectedDays)
await scheduleAlarm(alarm: alarm, weekdays: weekdays)
} catch {
print("Error saving alarm: \(error)")
}
}// Stop existing alarm
try await AlarmManager.shared.stop(id: existingAlarm.id)
// Update alarm properties
existingAlarm.name = newName
existingAlarm.snoozeDelays = newSnoozeDelays
// ... other updates
// Reschedule with new configuration
await scheduleAlarm(alarm: existingAlarm, weekdays: newWeekdays)private func deleteAlarms(offsets: IndexSet) {
for index in offsets {
let alarm = alarms[index]
modelContext.delete(alarm)
// Stop the scheduled alarm
Task {
try await AlarmManager.shared.stop(id: alarm.id)
}
}
}Always check authorization status before scheduling:
@MainActor
private func requestAlarmAuthorization() async throws {
let status = try await AlarmManager.shared.requestAuthorization()
switch status {
case .authorized:
break
case .denied, .notDetermined:
throw AlarmError.permissionDenied
@unknown default:
throw AlarmError.permissionDenied
}
}Implement comprehensive error handling:
enum AlarmError: Error, Sendable {
case permissionDenied
case schedulingFailed
case invalidConfiguration
}Use modular components for maintainability:
AlarmDetailsSection: Name and time configurationDaysSelectionSection: Day selection with toggle logicSnoozeDelaySection: Single-selection snooze durationAlarmColorsSection: Color customization with preview
Validate alarm configuration before scheduling:
.disabled(alarmName.isEmpty || selectedDays.isEmpty)The app supports a single configurable snooze duration:
let countdownDuration = Alarm.CountdownDuration(
preAlert: nil,
postAlert: TimeInterval(alarm.snoozeDelay)
)Implement AlarmMetadata for additional data:
struct ADHDMetadata: AlarmMetadata {
// Add custom properties as needed
init() {}
}For countdown display, implement Live Activities:
// Live Activity configuration would go here
// Required for countdown functionalitySolution: Ensure you're using the correct AlarmKit API parameters:
AlarmPresentation.Countdownonly acceptstitleandpauseButtonAlarmManager.AlarmConfigurationuses direct initializer, not.alarm()static method- Use
preAlert: nilinstead ofpreAlert: TimeInterval(0)inAlarm.CountdownDuration
Solution: Use hex string storage with computed properties for SwiftData compatibility
Solution: Configure postAlert interval in Alarm.CountdownDuration and set secondaryButtonBehavior: .countdown
- Apple Developer Documentation - AlarmKit
- WWDC 2025 - Wake up to the AlarmKit API
- SwiftData Documentation
This project serves as a comprehensive example of AlarmKit implementation. Feel free to use this code as a reference for your own AlarmKit integrations.
MIT License - Feel free to use this code in your own projects.
Note: This implementation requires iOS 26.0+ and demonstrates the complete AlarmKit API usage as of WWDC 2025. The framework provides system-level alarm capabilities that were previously exclusive to Apple's built-in Clock app.