BS
BleepingSwift
Published on

> Scheduling Alarms with AlarmKit

Authors
  • avatar
    Name
    Mick MacCallum
    Twitter
    @0x7fs

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 that show up in the Clock app and use the system's alarm infrastructure.

The framework is straightforward: you request authorization, create an alarm with a time and optional recurrence, and schedule it through AlarmManager. The alarm then appears alongside the user's other alarms in Clock.

Requesting Authorization

Before scheduling alarms, you need the user's permission. AlarmKit uses a familiar authorization pattern:

import AlarmKit

func requestAlarmAccess() async -> Bool {
    let manager = AlarmManager.shared

    do {
        let status = try await manager.requestAuthorization()
        return status == .authorized
    } catch {
        print("Authorization failed: \(error)")
        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.

Scheduling an Alarm

With authorization in place, you can create and schedule alarms. Each alarm needs at minimum a time, specified as DateComponents:

import AlarmKit

func scheduleWakeUpAlarm() async throws {
    let manager = AlarmManager.shared

    var time = DateComponents()
    time.hour = 7
    time.minute = 30

    let alarm = AlarmAttributes(
        time: time,
        title: "Wake Up"
    )

    try await manager.schedule(alarm)
}

The alarm now appears in the Clock app with your specified title. When it fires, the user gets the standard alarm experience with sound, snooze options, and the familiar dismiss interface.

Recurring Alarms

For alarms that repeat on specific days, add a recurrence rule:

import AlarmKit

func scheduleDailyAlarm() async throws {
    let manager = AlarmManager.shared

    var time = DateComponents()
    time.hour = 6
    time.minute = 0

    // Repeat on weekdays
    let recurrence = AlarmRecurrenceRule(
        daysOfWeek: [.monday, .tuesday, .wednesday, .thursday, .friday]
    )

    let alarm = AlarmAttributes(
        time: time,
        title: "Weekday Alarm",
        recurrenceRule: recurrence
    )

    try await manager.schedule(alarm)
}

You can also specify a single day for weekly recurrence, or omit the recurrence rule entirely for one-time alarms.

Managing Scheduled Alarms

Your app can fetch and manage the alarms it has scheduled:

import AlarmKit

func listMyAlarms() async throws -> [Alarm] {
    let manager = AlarmManager.shared
    return try await manager.scheduledAlarms()
}

func deleteAlarm(_ alarm: Alarm) async throws {
    let manager = AlarmManager.shared
    try await manager.cancel(alarm.id)
}

func deleteAllAlarms() async throws {
    let manager = AlarmManager.shared
    let alarms = try await manager.scheduledAlarms()

    for alarm in alarms {
        try await manager.cancel(alarm.id)
    }
}

Note that you can only access alarms your app created. Alarms from other apps or manually created in Clock aren't visible to your app.

Handling Alarm Events

To respond when your alarm fires or gets dismissed, implement an AlarmHandler:

import AlarmKit

class MyAlarmHandler: AlarmHandler {
    func alarmDidFire(_ alarm: Alarm) {
        // Called when the alarm goes off
        print("Alarm fired: \(alarm.attributes.title ?? "Untitled")")
    }

    func alarmWasDismissed(_ alarm: Alarm) {
        // Called when user dismisses or snoozes
        print("Alarm dismissed: \(alarm.id)")
    }
}

Register your handler when the app launches:

import SwiftUI
import AlarmKit

@main
struct MyApp: App {
    init() {
        AlarmManager.shared.handler = MyAlarmHandler()
    }

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

Practical Example

Here's a simple alarm scheduling view that ties everything together:

import SwiftUI
import AlarmKit

struct AlarmSchedulerView: View {
    @State private var alarmTime = Date()
    @State private var alarmTitle = ""
    @State private var isAuthorized = false
    @State private var scheduledAlarms: [Alarm] = []

    var body: some View {
        Form {
            Section("New Alarm") {
                DatePicker("Time", selection: $alarmTime, displayedComponents: .hourAndMinute)
                TextField("Title", text: $alarmTitle)

                Button("Schedule Alarm") {
                    Task { await scheduleAlarm() }
                }
                .disabled(!isAuthorized || alarmTitle.isEmpty)
            }

            Section("Scheduled Alarms") {
                ForEach(scheduledAlarms, id: \.id) { alarm in
                    HStack {
                        Text(alarm.attributes.title ?? "Untitled")
                        Spacer()
                        Text(formatTime(alarm.attributes.time))
                            .foregroundStyle(.secondary)
                    }
                }
                .onDelete(perform: deleteAlarms)
            }
        }
        .task {
            isAuthorized = await requestAuthorization()
            await refreshAlarms()
        }
    }

    func requestAuthorization() async -> Bool {
        do {
            let status = try await AlarmManager.shared.requestAuthorization()
            return status == .authorized
        } catch {
            return false
        }
    }

    func scheduleAlarm() async {
        let calendar = Calendar.current
        let components = calendar.dateComponents([.hour, .minute], from: alarmTime)

        let attributes = AlarmAttributes(
            time: components,
            title: alarmTitle
        )

        do {
            try await AlarmManager.shared.schedule(attributes)
            alarmTitle = ""
            await refreshAlarms()
        } catch {
            print("Failed to schedule: \(error)")
        }
    }

    func refreshAlarms() async {
        do {
            scheduledAlarms = try await AlarmManager.shared.scheduledAlarms()
        } catch {
            scheduledAlarms = []
        }
    }

    func deleteAlarms(at offsets: IndexSet) {
        Task {
            for index in offsets {
                let alarm = scheduledAlarms[index]
                try? await AlarmManager.shared.cancel(alarm.id)
            }
            await refreshAlarms()
        }
    }

    func formatTime(_ components: DateComponents) -> String {
        let formatter = DateFormatter()
        formatter.timeStyle = .short

        var date = Calendar.current.date(from: components) ?? Date()
        return formatter.string(from: date)
    }
}

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, just like regular Clock alarms. The system handles all the sound playback and UI, so you don't need to manage audio sessions or implement a custom alarm interface.

Users can edit or delete your app's alarms directly in the Clock app. If they do, your next call to scheduledAlarms() will reflect those changes. There's no callback for external modifications, so check the current state when your app becomes active if you need to stay in sync.

For details on the full API surface, see Apple's AlarmKit documentation.

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.