β° 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
- System Integration: μκ³ μ±μ μλκ³Ό μλ²½νκ² ν΅ν©λμ΄μΌ ν¨
- λͺ νν μ 보: μλ μκ°, λ°λ³΅ μ€μ μ λͺ νν νμ
- μ¬μ΄ ν κΈ: μλ νμ±ν/λΉνμ±νλ₯Ό λΉ λ₯΄κ² μ ν
- μ€λμ¦ Supported: νμ€ 9λΆ μ€λμ¦ μ 곡
- Critical Alert: μ€μν μλμ Critical Alert μ¬μ© κ³ λ €
π― 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.