← All articles
4 min readManav

Scheduling Alarms in iOS Apps with AlarmKit: A Complete Guide

The introduction of AlarmKit in iOS has revolutionized how developers can integrate alarm functionality into their applications. This powerful framework allows apps to schedule system-level alarms…

swiftuiswiftios

AlarmKit arrived in iOS and changed how an app integrates alarm functionality. Apps can now schedule system-level alarms. Those alarms persist even when the app isn’t running, which is what makes wake-up calls and reminders coming out of third-party software something a user can actually lean on.

#Understanding AlarmKit

Apple’s framework lets apps create and manage alarms through the system’s own alarm infrastructure. Local notifications never reach that layer. An AlarmKit alarm lands in the native Clock app instead, on scheduling solid enough that people will trust it for critical wake-up times.

Several things here beat the notification-based approach. Alarms you create with AlarmKit show up in the system’s Clock app, which means users manage them right alongside their regular ones.

Do Not Disturb gets respected the way it should, and behaviour stays consistent across the system.

#Key Components

A few components split the work between them:

AlarmManager coordinates everything: authorization, scheduling, state.

AlarmConfiguration is where behaviour and presentation get defined, and that covers countdown durations, schedules, and visual attributes. AlarmPresentation decides what the user sees, with separate configurations for the alert, countdown, and paused states.

#Basic Setup and Authorization

import AlarmKit
import SwiftUI

@Observable class ViewModel {
    @ObservationIgnored private let alarmManager = AlarmManager.shared
    @MainActor var alarmsMap = [UUID: (Alarm, LocalizedStringResource)]()

    private func requestAuthorization() async -> Bool {
        switch alarmManager.authorizationState {
        case .notDetermined:
            do {
                let state = try await alarmManager.requestAuthorization()
                return state == .authorized
            } catch {
                print("Error occurred while requesting authorization: \(error)")
                return false
            }
        case .denied: return false
        case .authorized: return true
        @unknown default: return false
        }
    }
}

#Scheduling Different Types of Alarms

Different use cases want different configurations. Main patterns:

Alert-Only Alarms cover the simple notification case:

func scheduleAlertOnlyExample() {
    let alertContent = AlarmPresentation.Alert(title: "Wake Up", stopButton: .stopButton)

    let attributes = AlarmAttributes(presentation: AlarmPresentation(alert: alertContent),
                                                  tintColor: Color.accentColor)

    let alarmConfiguration = AlarmConfiguration(schedule: .twoMinsFromNow, attributes: attributes)

    scheduleAlarm(id: UUID(), label: "Wake Up", alarmConfiguration: alarmConfiguration)
}

Countdown Alarms add pre-alert timing and repeat:

func scheduleCountdownAlertExample() {
    let alertContent = AlarmPresentation.Alert(title: "Food Ready",
                                               stopButton: .stopButton,
                                               secondaryButton: .repeatButton,
                                               secondaryButtonBehavior: .countdown)

    let countdownContent = AlarmPresentation.Countdown(title: "Cooking", pauseButton: .pauseButton)
    let pausedContent = AlarmPresentation.Paused(title: "Paused", resumeButton: .resumeButton)

    let attributes = AlarmAttributes(presentation: AlarmPresentation(alert: alertContent,
                                                                    countdown: countdownContent,
                                                                    paused: pausedContent),
                                     metadata: CookingData(method: .oven),
                                     tintColor: Color.accentColor)

    let alarmConfiguration = AlarmConfiguration(countdownDuration: .init(preAlert: 15 * 60, postAlert: 15 * 60),
                                                attributes: attributes)

    scheduleAlarm(id: UUID(), label: "Food is cooking", alarmConfiguration: alarmConfiguration)
}

Custom Button Alarms can launch your app directly:

func scheduleCustomButtonAlertExample() {
    let alertContent = AlarmPresentation.Alert(title: "Wake Up",
                                               stopButton: .stopButton,
                                               secondaryButton: .openAppButton,
                                               secondaryButtonBehavior: .custom)

    let attributes = AlarmAttributes(presentation: AlarmPresentation(alert: alertContent),
                                                  tintColor: Color.accentColor)

    let alarmConfiguration = AlarmConfiguration(schedule: .twoMinsFromNow,
                                                attributes: attributes,
                                                secondaryIntent: OpenAlarmAppIntent(alarmID: id.uuidString))

    scheduleAlarm(id: id, label: "Wake Up", alarmConfiguration: alarmConfiguration)
}

#Managing Alarm State

State updates arrive in real time, over an async sequence:

private func observeAlarms() {
    Task {
        for await incomingAlarms in alarmManager.alarmUpdates {
            updateAlarmState(with: incomingAlarms)
        }
    }
}

private func updateAlarmState(with remoteAlarms: [Alarm]) {
    Task { @MainActor in
        remoteAlarms.forEach { updated in
            alarmsMap[updated.id, default: (updated, "Alarm (Old Session)")].0 = updated
        }

        let knownAlarmIDs = Set(alarmsMap.keys)
        let incomingAlarmIDs = Set(remoteAlarms.map(\.id))

        let removedAlarmIDs = Set(knownAlarmIDs.subtracting(incomingAlarmIDs))
        removedAlarmIDs.forEach {
            alarmsMap[$0] = nil
        }
    }
}

#SwiftUI Integration

SwiftUI picks it up through the Observable pattern:

struct ContentView: View {
    @State private var viewModel = ViewModel()
    @State private var showAddSheet = false

    var body: some View {
        NavigationStack {
            if viewModel.hasUpcomingAlerts {
                alarmList(alarms: Array(viewModel.alarmsMap.values))
            } else {
                ContentUnavailableView("No Alarms",
                                     systemImage: "clock.badge.exclamationmark",
                                     description: Text("Add a new alarm by tapping + button."))
            }
        }
        .environment(viewModel)
        .onAppear {
            viewModel.fetchAlarms()
        }
    }
}

#Advanced Features

Scheduling goes deeper than one fire time: fixed dates and relative times both work, and you can hang custom metadata off an alarm so contextual information survives app launches. Buttons take custom text, colors, and system images. Secondary intents fire app-specific actions when someone interacts with the alarm.

#Best Practices

Request authorization first. Always, before any scheduling call, and handle each state that comes back instead of assuming the answer. Labels should mean something to the person reading them, and tint colors should match the rest of your app.

Scheduling fails sometimes; handle it. Edge cases include hitting the system limit on how many alarms are already scheduled. Persistence is managed for you, but keep your own state anyway or your UI drifts out of sync with what the system thinks it has.

#Conclusion

Alarms on iOS moved forward here. Follow the patterns Apple demonstrates in its sample code, keep state management honest, and what you ship is timing that users will trust for the things they cannot miss.

Living inside the native Clock app keeps the experience consistent without costing you customization or app-specific behaviour. Productivity app, cooking timer, fitness reminder: same foundation underneath, sitting inside iOS instead of beside it.

Found this useful? Share it.

Keep reading