BS
BleepingSwift
Published on
6 min read

> Scheduling Alarms with AlarmKit

Share:

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:

Swift
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:

Swift
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:

Swift
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:

Swift
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:

Swift
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:

Swift
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:):

Swift
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:

Swift
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:

Swift
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:

View 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.

subscribe.sh

// Stay Updated

Get notified when I publish new tutorials on Swift, SwiftUI, and iOS development. No spam, unsubscribe anytime.

>

By subscribing, you agree to our Privacy Policy.