🌐 KO

⏰ AlarmKit

⭐ Difficulty: ⭐⭐ ⏱️ Est. Time: 1h πŸ“‚ iOS 26

Alarm management perfectly integrated with the system Clock app

iOS 18+πŸ†• 2024

✨ What is AlarmKit?

AlarmKit is a framework introduced in iOS 18 that enables third-party apps to provide alarm functionality on par with the system Clock app. Create, edit, and delete alarms from your app, integrated into the Clock app's alarm list. It supports all native alarm features β€” recurring alarms, snooze, custom sounds β€” delivering a consistent experience.

πŸ’‘ Key Features: System Alarm Integration Β· Recurring Alarms Β· Snooze Settings Β· Custom Alarm Sounds Β· Vibration Patterns Β· Labels & Notes Β· iCloud Sync Β· Live Activity Support

🎯 1. Basic Setup

Initialize AlarmKit and request permissions.

AlarmManager.swift β€” Basic Setup
import SwiftUI
import UserNotifications

// AlarmKit κ΄€λ¦¬μž
@Observable
class AlarmManager {
    var alarms: [Alarm] = []
    var isAuthorized = false

    // μ•Œλ¦Ό κΆŒν•œ μš”μ²­
    func requestAuthorization() async -> Bool {
        do {
            let granted = try await UNUserNotificationCenter.current()
                .requestAuthorization(options: [.alert, .sound, .criticalAlert])

            await MainActor.run {
                isAuthorized = granted
            }

            return granted
        } catch {
            print("❌ κΆŒν•œ μš”μ²­ μ‹€νŒ¨: \(error)")
            return false
        }
    }

    // μ•ŒλžŒ λͺ©λ‘ 뢈러였기
    func loadAlarms() async {
        // μ‹œμŠ€ν…œ μ•ŒλžŒκ³Ό ν†΅ν•©λœ μ•ŒλžŒ λͺ©λ‘
        // μ‹€μ œλ‘œλŠ” AlarmKit API μ‚¬μš©
        alarms = [
            Alarm(
                time: Date(),
                label: "기상 μ•ŒλžŒ",
                isEnabled: true,
                repeatDays: [1, 2, 3, 4, 5]
            )
        ]
    }
}

// μ•ŒλžŒ λͺ¨λΈ
struct Alarm: Identifiable {
    let id = UUID()
    var time: Date
    var label: String
    var isEnabled: Bool
    var repeatDays: [Int] // 0=μΌμš”μΌ, 1=μ›”μš”μΌ...
    var sound: AlarmSound = .default
    var snoozeEnabled: Bool = true
    var vibrationPattern: VibrationPattern = .default
}

enum AlarmSound: String, CaseIterable {
    case `default` = "κΈ°λ³Έ"
    case radar = "λ ˆμ΄λ”"
    case chimes = "μ°¨μž„λ²¨"
    case bells = "μ’…"
}

enum VibrationPattern: String, CaseIterable {
    case `default` = "κΈ°λ³Έ"
    case rapid = "λΉ λ₯Έ 진동"
    case heartbeat = "심μž₯ 박동"
}

⏰ 2. Create Alarm

Create a new alarm and register it with the system.

CreateAlarmView.swift β€” Alarm Creation
import SwiftUI

struct CreateAlarmView: View {
    @Environment(\.dismiss) var dismiss
    @State private var alarmTime = Date()
    @State private var label = ""
    @State private var selectedDays: Set<Int> = []
    @State private var selectedSound: AlarmSound = .default
    @State private var snoozeEnabled = true

    let weekdays = ["일", "μ›”", "ν™”", "수", "λͺ©", "금", "ν† "]

    var body: some View {
        NavigationStack {
            Form {
                Section {
                    // μ‹œκ°„ 선택
                    DatePicker(
                        "μ‹œκ°„",
                        selection: $alarmTime,
                        displayedComponents: .hourAndMinute
                    )
                    .datePickerStyle(.wheel)
                    .labelsHidden()
                }

                Section("라벨") {
                    TextField("μ•ŒλžŒ 이름", text: $label)
                }

                Section("반볡") {
                    HStack(spacing: 8) {
                        ForEach(0.<7, id: \.self) { day in
                            Button {
                                toggleDay(day)
                            } label: {
                                Text(weekdays[day])
                                    .font(.caption)
                                    .frame(width: 40, height: 40)
                                    .background(
                                        selectedDays.contains(day) ?
                                            Color.blue : Color.gray.opacity(0.2)
                                    )
                                    .foregroundStyle(
                                        selectedDays.contains(day) ? .white : .primary
                                    )
                                    .clipShape(Circle())
                            }
                        }
                    }
                    .buttonStyle(.plain)
                }

                Section("μ•ŒλžŒμŒ") {
                    Picker("μ†Œλ¦¬", selection: $selectedSound) {
                        ForEach(AlarmSound.allCases, id: \.self) { sound in
                            Text(sound.rawValue).tag(sound)
                        }
                    }
                }

                Section {
                    Toggle("μŠ€λˆ„μ¦ˆ", isOn: $snoozeEnabled)
                }
            }
            .navigationTitle("μ•ŒλžŒ μΆ”κ°€")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .cancellationAction) {
                    Button("μ·¨μ†Œ") {
                        dismiss()
                    }
                }

                ToolbarItem(placement: .confirmationAction) {
                    Button("μ €μž₯") {
                        saveAlarm()
                    }
                }
            }
        }
    }

    func toggleDay(_ day: Int) {
        if selectedDays.contains(day) {
            selectedDays.remove(day)
        } else {
            selectedDays.insert(day)
        }
    }

    func saveAlarm() {
        let alarm = Alarm(
            time: alarmTime,
            label: label.isEmpty ? "μ•ŒλžŒ" : label,
            isEnabled: true,
            repeatDays: Array(selectedDays).sorted(),
            sound: selectedSound,
            snoozeEnabled: snoozeEnabled
        )

        Task {
            await scheduleAlarm(alarm)
        }

        dismiss()
    }

    func scheduleAlarm(_ alarm: Alarm) async {
        // AlarmKit API둜 μ•ŒλžŒ 등둝
        print("⏰ μ•ŒλžŒ 생성: \(alarm.label) at \(alarm.time)")

        // UNNotificationRequest 생성
        let content = UNMutableNotificationContent()
        content.title = alarm.label
        content.sound = .default
        content.interruptionLevel = .timeSensitive

        // 반볡 μ„€μ •
        if alarm.repeatDays.isEmpty {
            // μΌνšŒμ„± μ•ŒλžŒ
            let components = Calendar.current.dateComponents(
                [.hour, .minute],
                from: alarm.time
            )
            let trigger = UNCalendarNotificationTrigger(
                dateMatching: components,
                repeats: false
            )

            let request = UNNotificationRequest(
                identifier: alarm.id.uuidString,
                content: content,
                trigger: trigger
            )

            try? await UNUserNotificationCenter.current().add(request)
        } else {
            // 반볡 μ•ŒλžŒ (각 μš”μΌλ§ˆλ‹€ 별도 등둝)
            for day in alarm.repeatDays {
                var components = Calendar.current.dateComponents(
                    [.hour, .minute],
                    from: alarm.time
                )
                components.weekday = day + 1 // 1=μΌμš”μΌ

                let trigger = UNCalendarNotificationTrigger(
                    dateMatching: components,
                    repeats: true
                )

                let request = UNNotificationRequest(
                    identifier: "\(alarm.id.uuidString)-\(day)",
                    content: content,
                    trigger: trigger
                )

                try? await UNUserNotificationCenter.current().add(request)
            }
        }
    }
}

πŸ“‹ 3. Alarm List

Display and manage all registered alarms.

AlarmListView.swift β€” Alarm List
import SwiftUI

struct AlarmListView: View {
    @State private var manager = AlarmManager()
    @State private var showCreateAlarm = false

    var body: some View {
        NavigationStack {
            List {
                if manager.alarms.isEmpty {
                    ContentUnavailableView {
                        Label("μ•ŒλžŒ μ—†μŒ", systemImage: "alarm")
                    } description: {
                        Text("+ λ²„νŠΌμ„ 눌러 μ•ŒλžŒμ„ μΆ”κ°€ν•˜μ„Έμš”")
                    }
                } else {
                    ForEach(manager.alarms) { alarm in
                        AlarmRow(alarm: alarm) {
                            toggleAlarm(alarm)
                        }
                    }
                    .onDelete { indexSet in
                        deleteAlarms(at: indexSet)
                    }
                }
            }
            .navigationTitle("μ•ŒλžŒ")
            .toolbar {
                ToolbarItem(placement: .topBarTrailing) {
                    Button {
                        showCreateAlarm = true
                    } label: {
                        Image(systemName: "plus")
                    }
                }
            }
            .sheet(isPresented: $showCreateAlarm) {
                CreateAlarmView()
            }
            .task {
                await manager.requestAuthorization()
                await manager.loadAlarms()
            }
        }
    }

    func toggleAlarm(_ alarm: Alarm) {
        guard let index = manager.alarms.firstIndex(where: { $0.id == alarm.id })
        else { return }

        manager.alarms[index].isEnabled.toggle()

        Task {
            if manager.alarms[index].isEnabled {
                await scheduleAlarm(manager.alarms[index])
            } else {
                await cancelAlarm(manager.alarms[index])
            }
        }
    }

    func deleteAlarms(at offsets: IndexSet) {
        for index in offsets {
            let alarm = manager.alarms[index]
            Task {
                await cancelAlarm(alarm)
            }
        }
        manager.alarms.remove(atOffsets: offsets)
    }

    func scheduleAlarm(_ alarm: Alarm) async {
        print("⏰ μ•ŒλžŒ ν™œμ„±ν™”: \(alarm.label)")
    }

    func cancelAlarm(_ alarm: Alarm) async {
        // μ•ŒλžŒ μ·¨μ†Œ
        var identifiers = [alarm.id.uuidString]

        // 반볡 μ•ŒλžŒμΈ 경우 λͺ¨λ“  μš”μΌ μ·¨μ†Œ
        for day in alarm.repeatDays {
            identifiers.append("\(alarm.id.uuidString)-\(day)")
        }

        UNUserNotificationCenter.current()
            .removePendingNotificationRequests(withIdentifiers: identifiers)

        print("πŸ”• μ•ŒλžŒ λΉ„ν™œμ„±ν™”: \(alarm.label)")
    }
}

struct AlarmRow: View {
    let alarm: Alarm
    let onToggle: () -> Void

    var timeString: String {
        let formatter = DateFormatter()
        formatter.timeStyle = .short
        return formatter.string(from: alarm.time)
    }

    var repeatText: String {
        if alarm.repeatDays.isEmpty {
            return "ν•œ 번"
        }

        let weekdays = ["일", "μ›”", "ν™”", "수", "λͺ©", "금", "ν† "]
        let dayNames = alarm.repeatDays.map { weekdays[$0] }

        // 맀일
        if alarm.repeatDays.count == 7 {
            return "맀일"
        }

        // 주쀑
        if Set(alarm.repeatDays) == Set([1, 2, 3, 4, 5]) {
            return "주쀑"
        }

        // 주말
        if Set(alarm.repeatDays) == Set([0, 6]) {
            return "주말"
        }

        return dayNames.joined(separator: ", ")
    }

    var body: some View {
        HStack {
            VStack(alignment: .leading, spacing: 4) {
                Text(timeString)
                    .font(.system(size: 40, weight: .light))
                    .foregroundStyle(alarm.isEnabled ? .primary : .secondary)

                if !alarm.label.isEmpty {
                    Text(alarm.label)
                        .font(.headline)
                }

                Text(repeatText)
                    .font(.subheadline)
                    .foregroundStyle(.secondary)
            }

            Spacer()

            Toggle("", isOn: Binding(
                get: { alarm.isEnabled },
                set: { _ in onToggle() }
            ))
            .labelsHidden()
        }
        .padding(.vertical, 8)
    }
}

⏱️ 4. Snooze

Handle snooze when the alarm fires.

SnoozeHandler.swift β€” Snooze Handler
import SwiftUI
import UserNotifications

@Observable
class SnoozeHandler: NSObject, UNUserNotificationCenterDelegate {
    var snoozeDuration: TimeInterval = 9 * 60 // 9λΆ„

    override init() {
        super.init()
        UNUserNotificationCenter.current().delegate = self
    }

    // μ•Œλ¦Όμ΄ 화면에 ν‘œμ‹œλ  λ•Œ
    func userNotificationCenter(
        _ center: UNUserNotificationCenter,
        willPresent notification: UNNotification
    ) async -> UNNotificationPresentationOptions {
        // μ•ŒλžŒ ν™”λ©΄ ν‘œμ‹œ
        return [.banner, .sound, .list]
    }

    // μ•Œλ¦Ό μ•‘μ…˜ 처리
    func userNotificationCenter(
        _ center: UNUserNotificationCenter,
        didReceive response: UNNotificationResponse
    ) async {
        switch response.actionIdentifier {
        case "SNOOZE_ACTION":
            await snoozeAlarm(response.notification)

        case "STOP_ACTION", UNNotificationDefaultActionIdentifier:
            stopAlarm(response.notification)

        default:
            break
        }
    }

    // μŠ€λˆ„μ¦ˆ μ•ŒλžŒ 등둝
    func snoozeAlarm(_ notification: UNNotification) async {
        let content = UNMutableNotificationContent()
        content.title = notification.request.content.title
        content.body = "μŠ€λˆ„μ¦ˆ"
        content.sound = .default
        content.interruptionLevel = .timeSensitive

        // 9λΆ„ ν›„ λ‹€μ‹œ μ•ŒλžŒ
        let trigger = UNTimeIntervalNotificationTrigger(
            timeInterval: snoozeDuration,
            repeats: false
        )

        let request = UNNotificationRequest(
            identifier: "\(notification.request.identifier)-snooze",
            content: content,
            trigger: trigger
        )

        try? await UNUserNotificationCenter.current().add(request)
        print("😴 μŠ€λˆ„μ¦ˆ: 9λΆ„ ν›„ λ‹€μ‹œ μ•ŒλžŒ")
    }

    // μ•ŒλžŒ 쀑지
    func stopAlarm(_ notification: UNNotification) {
        print("⏹️ μ•ŒλžŒ 쀑지")
    }
}

// μ•ŒλžŒ μ•‘μ…˜ μΉ΄ν…Œκ³ λ¦¬ 등둝
extension UNUserNotificationCenter {
    static func registerAlarmActions() {
        let snoozeAction = UNNotificationAction(
            identifier: "SNOOZE_ACTION",
            title: "μŠ€λˆ„μ¦ˆ",
            options: []
        )

        let stopAction = UNNotificationAction(
            identifier: "STOP_ACTION",
            title: "쀑지",
            options: [.destructive]
        )

        let category = UNNotificationCategory(
            identifier: "ALARM_CATEGORY",
            actions: [snoozeAction, stopAction],
            intentIdentifiers: [],
            options: [.customDismissAction]
        )

        UNUserNotificationCenter.current().setNotificationCategories([category])
    }
}

πŸ”” 5. Live Activity Integration

Display on Dynamic Island and Lock Screen when the alarm fires.

AlarmActivityView.swift β€” Live Activity
import SwiftUI
import ActivityKit

// μ•ŒλžŒ Activity 속성
struct AlarmActivityAttributes: ActivityAttributes {
    public struct ContentState: Codable, Hashable {
        var timeRemaining: TimeInterval
        var isSnoozed: Bool
    }

    var alarmLabel: String
    var alarmTime: Date
}

// Dynamic Island λ·°
struct AlarmActivityView: View {
    let context: ActivityViewContext<AlarmActivityAttributes>

    var body: some View {
        VStack {
            HStack {
                Image(systemName: "alarm.fill")
                    .foregroundStyle(.orange)

                VStack(alignment: .leading) {
                    Text(context.attributes.alarmLabel)
                        .font(.headline)

                    if context.state.isSnoozed {
                        Text("μŠ€λˆ„μ¦ˆλ¨")
                            .font(.caption)
                            .foregroundStyle(.secondary)
                    }
                }

                Spacer()

                Text(timeRemainingText)
                    .font(.title2)
                    .fontWeight(.semibold)
                    .monospacedDigit()
            }
            .padding()
        }
    }

    var timeRemainingText: String {
        let remaining = Int(context.state.timeRemaining)
        let minutes = remaining / 60
        let seconds = remaining % 60
        return String(format: "%02d:%02d", minutes, seconds)
    }
}

// Live Activity μ‹œμž‘
func startAlarmActivity(alarm: Alarm) async {
    let attributes = AlarmActivityAttributes(
        alarmLabel: alarm.label,
        alarmTime: alarm.time
    )

    let initialState = AlarmActivityAttributes.ContentState(
        timeRemaining: 60,
        isSnoozed: false
    )

    do {
        let activity = try Activity.request(
            attributes: attributes,
            content: .init(state: initialState, staleDate: nil)
        )

        print("βœ… Live Activity μ‹œμž‘: \(activity.id)")
    } catch {
        print("❌ Live Activity μ‹€νŒ¨: \(error)")
    }
}

🎡 6. Custom Alarm Sounds

Custom alarm sounds and vibration patterns configuration.

CustomSoundPicker.swift β€” Alarm Sound Picker
import SwiftUI
import AVFoundation

struct CustomSoundPickerView: View {
    @Binding var selectedSound: AlarmSound
    @State private var audioPlayer: AVAudioPlayer?

    var body: some View {
        List {
            Section("κΈ°λ³Έ μ•ŒλžŒμŒ") {
                ForEach(AlarmSound.allCases, id: \.self) { sound in
                    HStack {
                        Text(sound.rawValue)

                        Spacer()

                        if selectedSound == sound {
                            Image(systemName: "checkmark")
                                .foregroundStyle(.blue)
                        }

                        Button {
                            previewSound(sound)
                        } label: {
                            Image(systemName: "play.circle")
                        }
                        .buttonStyle(.borderless)
                    }
                    .contentShape(Rectangle())
                    .onTapGesture {
                        selectedSound = sound
                    }
                }
            }

            Section("μ»€μŠ€ν…€ μ•ŒλžŒμŒ") {
                Button {
                    importCustomSound()
                } label: {
                    Label("μ‚¬μš΄λ“œ κ°€μ Έμ˜€κΈ°", systemImage: "plus.circle")
                }
            }
        }
        .navigationTitle("μ•ŒλžŒμŒ")
    }

    func previewSound(_ sound: AlarmSound) {
        // μ‹œμŠ€ν…œ μ‚¬μš΄λ“œ μž¬μƒ
        AudioServicesPlaySystemSound(1005) // μ˜ˆμ‹œ
    }

    func importCustomSound() {
        print("🎡 μ»€μŠ€ν…€ μ‚¬μš΄λ“œ κ°€μ Έμ˜€κΈ°")
    }
}

πŸ’‘ HIG Guidelines

🎯 Practical Usage

πŸ“š Learn More

⚑️ 팁: AlarmKit integrates seamlessly with the system Clock app. Alarms created in your app are visible in the Clock app and can be managed from anywhere. Request Critical Alert permission to sound alarms even in silent mode. Use Live Activity to show alarm status in real-time on Dynamic Island.

πŸ“Ž Apple Official Resources

πŸ“˜ Documentation 🎬 WWDC Sessions