AlarmKit di iOS 26: Panduan Lengkap Bikin Custom Alarm dan Timer dengan Swift
AlarmKit di iOS 26 memungkinkan aplikasi pihak ketiga bikin alarm dan timer yang menembus mode Focus dan silent. Panduan ini menunjukkan cara memakai AlarmManager, App Intents, dan AlarmPresentation dengan contoh kode Swift lengkap.
AlarmKit di iOS 26 adalah framework native Apple yang memungkinkan aplikasi pihak ketiga menjadwalkan alarm dan timer yang berbunyi bahkan saat perangkat dalam mode Focus atau silent, tanpa perlu mengandalkan push notification. Framework ini menggantikan trik lama dengan UNCalendarNotificationTrigger atau audio background untuk kasus alarm asli, dan menyediakan API deklaratif berbasis AlarmManager.shared, AlarmAttributes, dan AlarmPresentation. Jujur saja, saya sudah menghabiskan beberapa minggu mengintegrasikannya ke aplikasi rutinitas tidur di iPhone dan Apple Watch, dan panduan ini merangkum semua yang saya harap sudah saya ketahui sebelum mulai.
AlarmKit adalah framework baru di iOS 26, iPadOS 26, dan watchOS 26 untuk membuat alarm dan timer custom yang menembus mode Focus dan silent.
Semua alarm dikelola lewat singleton AlarmManager.shared, dan aplikasi wajib memanggil requestAuthorization() serta menambahkan kunci NSAlarmKitUsageDescription di Info.plist.
Ada dua bentuk pemicu: Alarm.Schedule untuk waktu absolut/relatif dan Alarm.Countdown untuk timer hitung mundur, keduanya bisa digabung dalam satu AlarmConfiguration.
Kustomisasi tampilan alarm memakai AlarmPresentation dengan tiga state utama: alert, countdown, dan paused, dirender pada Dynamic Island, Lock Screen, dan StandBy.
Aksi seperti snooze dan stop dihubungkan lewat App Intents, sehingga tombol di UI alarm memicu LiveActivityIntent yang berjalan tanpa membuka aplikasi.
Di Apple Watch, AlarmKit muncul sebagai smart stack widget dan haptic khusus, sementara di iPad memakai layout compact yang menyesuaikan Stage Manager.
Apa itu AlarmKit dan kenapa framework ini penting di iOS 26?
AlarmKit adalah framework baru yang diperkenalkan Apple di WWDC 2025 dan tersedia mulai iOS 26, iPadOS 26, serta watchOS 26. Sebelum framework ini muncul, developer harus memanfaatkan UNNotificationSound.criticalSoundNamed atau audio silent yang berjalan di background untuk meniru perilaku alarm, dan tidak satu pun dari cara itu benar-benar setara dengan aplikasi Clock bawaan. Dengan AlarmKit, sistem memperlakukan alarm dari aplikasi pihak ketiga sebagai warga kelas satu: bunyi menembus Silent Switch dan mode Focus, muncul dengan tombol Stop/Snooze di Lock Screen, tampil di Dynamic Island, dan bahkan bisa dipicu di jam tangan.
Dalam praktik, framework ini berguna untuk aplikasi meditasi, rutinitas tidur, memasak, kebugaran, produktivitas Pomodoro, sampai fitur wake-up call di aplikasi perjalanan. Yang perlu dipahami sejak awal: AlarmKit dirancang untuk alarm yang diinginkan pengguna. Apple secara tegas melarang penggunaan API ini untuk promosi, pengingat marketing, atau notifikasi tak diminta, dan App Review akan menolak aplikasi yang menyalahgunakannya. Selain itu, AlarmKit tidak sepenuhnya menggantikan UserNotifications; framework tersebut tetap dipakai untuk reminder ringan dan silent push. Pemahaman soal batasan ini akan membentuk keputusan arsitektur berikutnya.
Persiapan proyek: Info.plist dan meminta izin AlarmKit
Langkah pertama sebelum baris kode apa pun adalah menyiapkan konfigurasi proyek. AlarmKit memerlukan kunci NSAlarmKitUsageDescription di Info.plist yang berisi penjelasan singkat, ramah pengguna, tentang alasan aplikasi membutuhkan alarm. Tanpa kunci ini, pemanggilan API mana pun akan melempar error dan sistem tidak akan menampilkan prompt izin. Contoh nilai yang saya pakai di aplikasi rutinitas tidur: "Digunakan untuk membangunkan kamu di waktu yang sudah dijadwalkan, walau perangkat sedang dalam mode Focus."
Setelah Info.plist siap, kamu perlu meminta otorisasi eksplisit dari pengguna lewat AlarmManager.shared.requestAuthorization(). Otorisasi ini terpisah dari UNUserNotificationCenter. Status .authorized untuk notifikasi tidak berarti aplikasi otomatis mendapat izin alarm. Lakukan permintaan di titik yang kontekstual, misalnya saat pengguna pertama kali mengatur alarm, bukan di splash screen. Berikut kode setup lengkap yang bisa kamu tempel di sebuah service class:
import AlarmKit
import Foundation
@Observable
final class AlarmService {
enum AuthorizationState {
case notDetermined, authorized, denied
}
private(set) var authorization: AuthorizationState = .notDetermined
private let manager = AlarmManager.shared
// Panggil sekali saat aplikasi berjalan atau saat pengguna menekan tombol "Aktifkan Alarm".
func syncAuthorization() async {
switch manager.authorizationState {
case .authorized:
authorization = .authorized
case .denied:
authorization = .denied
case .notDetermined:
do {
let result = try await manager.requestAuthorization()
authorization = result == .authorized ? .authorized : .denied
} catch {
authorization = .denied
print("AlarmKit auth error: \(error)")
}
@unknown default:
authorization = .denied
}
}
}
Perhatikan bahwa AlarmManager.shared aman diakses dari mana saja karena ditandai Sendable, dan cocok dipadukan dengan pola Approachable Concurrency Swift 6.2 yang saya bahas sebelumnya. Kalau kamu masih memakai ObservableObject lama, migrasikan dulu ke @Observable supaya UI merespons perubahan state otorisasi tanpa boilerplate.
Cara bikin countdown alarm dengan AlarmManager
Countdown adalah bentuk alarm paling sederhana di AlarmKit. Kamu menentukan durasi target (misalnya 25 menit untuk Pomodoro), lalu framework menangani hitung mundur, notifikasi, dan tampilan aktif. Objek utamanya adalah Alarm.Countdown, yang dibungkus dalam AlarmConfiguration bersama AlarmAttributes. Aturan pentingnya: durasi maksimal 24 jam, dan hanya boleh ada satu alarm aktif per jenis countdown yang kamu tandai dengan Alarm.Metadata unik.
Berikut contoh membuat timer Pomodoro 25 menit yang saya pakai di aplikasi fokus:
import AlarmKit
struct PomodoroMetadata: AlarmMetadata {
let taskTitle: String
let session: Int
}
func startPomodoro(taskTitle: String, session: Int) async throws {
let countdown = Alarm.Countdown(
preAlert: .init(duration: 25 * 60), // 25 menit sampai bunyi
postAlert: .init(duration: 5 * 60) // 5 menit break setelah stop
)
let attributes = AlarmAttributes(
presentation: PomodoroAlarmPresentation.default,
metadata: PomodoroMetadata(taskTitle: taskTitle, session: session),
tintColor: .indigo
)
let configuration = AlarmConfiguration(
countdownDuration: countdown,
attributes: attributes,
stopIntent: StopPomodoroIntent(), // App Intent, lihat bagian bawah
secondaryIntent: PausePomodoroIntent(),
secondaryButtonBehavior: .custom
)
_ = try await AlarmManager.shared.schedule(
id: UUID(),
configuration: configuration
)
}
Ada beberapa hal krusial. Pertama, preAlert adalah durasi utama countdown. Kalau kamu isi postAlert, framework akan otomatis melanjutkan ke fase kedua setelah pengguna menekan tombol Stop, yang sangat pas untuk pola work/break. Kedua, metadata yang typed (di sini PomodoroMetadata) muncul di dalam view Live Activity dan bisa kamu decode kembali saat aplikasi dibuka. Ketiga, ID alarm harus unik. Simpan di UserDefaults atau SwiftData model inheritance kalau kamu ingin membatalkannya nanti dari UI.
Cara membuat schedule alarm untuk waktu tertentu
Kalau kebutuhan kamu adalah alarm yang berbunyi pada jam tertentu (misalnya 06:30 setiap hari kerja), pakai Alarm.Schedule. Objek ini mendukung waktu absolut menggunakan DateComponents, waktu relatif, dan pola pengulangan seperti "setiap Senin dan Jumat". API-nya cukup ekspresif dan mengeliminasi kebutuhan untuk menghitung tanggal berikutnya secara manual.
Beberapa catatan lapangan. Pertama, timezone. Selalu tetapkan timeZone eksplisit; kalau tidak, alarm bisa bergeser saat pengguna terbang lintas benua. Kedua, gunakan Alarm.Schedule.recurring hanya untuk pola yang tidak bisa dinyatakan sekali. Jangan buat 30 alarm harian terpisah untuk repeating alarm, sistem punya kuota alarm aktif per aplikasi (sekitar 50 di iOS 26 beta 4). Ketiga, jika pengguna mematikan alarm lewat UI iOS, framework akan mengirim delegate callback lewat AlarmManager.shared.alarmUpdates stream, yang idealnya kamu subscribe di root view supaya UI aplikasi sinkron.
Kustomisasi tampilan alarm dengan AlarmAttributes dan AlarmPresentation
Bagian yang paling menyenangkan (dan paling sering salah dikonfigurasi) adalah kustomisasi UI. AlarmKit memakai model presentasi yang sangat mirip dengan ActivityKit, tetapi dengan tiga state khusus alarm: alert, countdown, dan paused. Setiap state adalah SwiftUI View yang dibuat oleh sistem (bukan aplikasi kamu), sehingga harus deklaratif, tanpa side effect, dan tidak boleh mengakses network atau file lokal. Ini penting untuk keamanan dan konsistensi tampilan di Dynamic Island, Lock Screen, dan StandBy.
Sistem akan otomatis memilih layout compact atau expanded berdasarkan konteks. Dynamic Island memakai versi compact, sementara Lock Screen versi expanded. Karena view ini dijalankan oleh ChronoWidgetRenderer di process terpisah, semua asset yang kamu pakai harus tersedia di widget bundle. Kalau kamu sudah familiar dengan Liquid Glass di SwiftUI, kamu bisa memanfaatkannya di sini: tambahkan .background(.regularMaterial, in: .rect(cornerRadius: 20)) supaya alarm nyambung dengan estetika iOS 26.
Integrasi App Intents untuk tombol Snooze dan Stop
Semua interaksi tombol di UI alarm harus melewati App Intents. Alasannya arsitektural: view dirender di process widget, jadi tidak bisa memanggil closure aplikasi utama. Kamu wajib menyediakan minimal satu LiveActivityIntent yang menangani Stop, dan opsional satu untuk Snooze atau aksi custom. Sistem menangani mapping tombol ke intent secara otomatis lewat properti stopIntent dan secondaryIntent di AlarmConfiguration.
import AppIntents
import AlarmKit
struct SnoozeWakeUpIntent: LiveActivityIntent {
static var title: LocalizedStringResource = "Snooze"
@Parameter(title: "Menit") var minutes: Int
init() { self.minutes = 9 }
init(minutes: Int) { self.minutes = minutes }
func perform() async throws -> some IntentResult {
// Batalkan alarm aktif lalu jadwalkan ulang setelah durasi snooze.
let manager = AlarmManager.shared
for alarm in try await manager.alarms where alarm.state == .alerting {
try await manager.cancel(id: alarm.id)
try await rescheduleAfter(minutes: minutes, basedOn: alarm)
}
return .result()
}
}
Beberapa jebakan yang saya temui. Pertama, jangan lupa mendaftarkan intent kamu di AppShortcutsProvider supaya Shortcuts app juga mengenalinya. Kedua, perform() hanya punya sekitar 10 detik untuk selesai. Segala operasi network sebaiknya di-queue dan dieksekusi lewat BGProcessingTask. Ketiga, kalau kamu memakai pola typed throws Swift 6.2, ubah signature perform() menjadi throws(AlarmActionError) supaya error dari AlarmManager bisa ditangkap dengan tepat di Shortcuts. Terakhir, cek dokumentasi resmi LiveActivityIntent untuk memastikan intent kamu tidak dikonfigurasi dengan openAppWhenRun, karena itu akan memaksa aplikasi utama untuk aktif dan memicu penolakan App Review untuk alarm.
Menangani lifecycle alarm: pause, resume, dan cancel
AlarmKit mengekspos state machine yang sederhana namun perlu dipahami: scheduled, countingDown, paused, alerting, dan ended. Perpindahan antar state bisa terjadi karena aksi pengguna (menekan Stop di Lock Screen), atau lewat panggilan API kamu. Semua perubahan disiarkan lewat AlarmManager.shared.alarmUpdates, sebuah AsyncSequence yang cocok dipakai di .task { … } modifier SwiftUI atau di root App.
struct RootView: View {
@Environment(AlarmService.self) private var service
@State private var activeAlarms: [Alarm] = []
var body: some View {
AlarmListView(alarms: activeAlarms)
.task {
for await update in AlarmManager.shared.alarmUpdates {
activeAlarms = update.alarms
}
}
}
}
Untuk menghentikan sementara alarm (misalnya saat pengguna menekan tombol "Pause" dari dalam aplikasi kamu), panggil AlarmManager.shared.pause(id:). Framework akan menyimpan sisa waktu. Saat kamu memanggil resume(id:), countdown lanjut dari titik itu. Untuk membatalkan permanen, gunakan cancel(id:). Yang sering terlewat: setelah cancel, kamu tetap perlu membersihkan referensi ID di persistence layer kamu, karena AlarmKit tidak menyimpan riwayat alarm yang dibatalkan. Kalau UI kamu memakai NavigationStack dengan type-safe routing, jadikan status alarm bagian dari path supaya deep link seperti myapp://alarm/<id>/edit otomatis dinonaktifkan saat alarm dibatalkan.
Perbedaan AlarmKit di iPhone, iPad, dan Apple Watch
Salah satu keunggulan AlarmKit adalah cakupan cross-platform yang konsisten, tetapi ada nuansa penting di setiap perangkat. Di iPhone, alarm memakai layout expanded penuh di Lock Screen dan Dynamic Island. Suara memanfaatkan haptic engine untuk membangunkan pengguna, dan pengguna bisa menyapu ke Stop atau menekan Snooze. iPad memakai layout yang sama, tetapi karena tidak ada Dynamic Island, presentasi minimized muncul di StandBy atau Lock Screen. Stage Manager tidak memengaruhi alarm; mereka selalu muncul sebagai overlay di atas semua window.
Di Apple Watch, ceritanya berbeda. AlarmKit terintegrasi dengan Smart Stack dan menampilkan complication khusus. Jika pengguna hanya memakai jam tangan, alarm tetap berbunyi meski iPhone tidak dekat, asal watch memiliki koneksi ke jaringan seluler atau Wi-Fi cadangan untuk sinkronisasi. Karena ukuran layar terbatas, hanya AlarmPresentation.Alert yang dirender; countdown view kamu diabaikan, sebagai gantinya sistem menampilkan timer bawaan. Ini artinya semua metadata penting yang ingin pengguna lihat harus muat di alert.title dan alert.body, bukan di countdown. Saya biasanya menulis dua varian view: satu untuk iPhone/iPad yang detail, dan satu ringkas khusus untuk watchOS.
AlarmKit vs UserNotifications vs ActivityKit: kapan pakai apa?
Pertanyaan yang paling sering muncul di forum developer: kalau saya bisa memakai UserNotifications untuk pengingat dan ActivityKit untuk Live Activity, mengapa perlu framework baru? Jawaban singkatnya, tujuan mereka berbeda. Tabel di bawah merangkum perbedaan utama yang saya pakai untuk memutuskan framework mana yang tepat di setiap fitur.
Aspek
AlarmKit
UserNotifications
ActivityKit
Tujuan utama
Alarm dan timer wajib bunyi
Pengingat, badge, banner
Update real-time (skor, delivery)
Menembus Focus / silent
Ya, selalu
Hanya dengan critical alerts (perlu entitlement khusus)
Tidak; hanya visual
Suara custom
Ya, panjang, looping
Ya, maksimal 30 detik
Tidak
UI kustom
Alert, Countdown, Paused view
Notification Content Extension terbatas
Dynamic Island + Lock Screen
Aksi tombol
App Intents (Snooze, Stop, custom)
Notification Actions statis
App Intents
Batasan durasi
Hingga 24 jam per alarm
Tidak ada
8 jam Live Activity
Platform
iOS 26, iPadOS 26, watchOS 26
Semua platform Apple
iOS 16.1+, iPadOS 17+
Aturan praktis saya: pakai AlarmKit untuk apa pun yang wajib bunyi dan berorientasi waktu tunggal; UserNotifications untuk pengingat lunak dan reminder yang boleh dibungkam; ActivityKit untuk update progress yang berjalan bersamaan dengan alarm. Menariknya, AlarmKit sendiri memakai infrastruktur ActivityKit di belakang layar untuk merender countdown view-nya, jadi mempelajari salah satu akan mempercepat pemahaman yang lain. Kalau kamu ingin tinjauan resmi tentang bagaimana ketiga framework ini berbagi runtime, sesi WWDC25 "Wake up to AlarmKit" memberikan gambaran arsitektur yang solid.
Kesalahan umum dan cara debugging AlarmKit
Selama beta, saya mengumpulkan daftar bug dan kesalahan konfigurasi yang paling sering muncul. Yang pertama adalah alarm yang terjadwal tetapi tidak berbunyi. 90% kasusnya adalah NSAlarmKitUsageDescription lupa ditambahkan, atau simulator dijalankan tanpa Trust This Computer. Alarm hanya berjalan di device fisik yang memiliki Time-Sensitive interrupt aktif. Simulator memakai stub yang mengirim event tetapi tidak selalu memutar audio.
Kesalahan kedua yang sering muncul: Alarm.Countdown yang durasinya di atas 24 jam. Framework melempar AlarmManager.Error.durationExceedsLimit tanpa pesan yang jelas. Selalu validasi input pengguna sebelum memanggil schedule. Ketiga, view di AlarmPresentation yang memakai @State. Ini tidak diperbolehkan karena view dirender di process widget dengan snapshot statis, jadi pakai context.state untuk semua data dinamis. Untuk debugging umum, aktifkan sysdiagnose lewat xcrun devicectl lalu filter log subsystem com.apple.AlarmKit. Kalau masih buntu, cek breaking change dan issue di dokumentasi resmi Apple AlarmKit yang di-update setiap rilis beta.
Pertanyaan yang Sering Ditanyakan
Apakah AlarmKit gratis untuk semua developer?
Ya, AlarmKit adalah bagian dari SDK iOS 26 dan tidak membutuhkan entitlement berbayar. Namun App Review sangat ketat: aplikasi yang memakai framework ini untuk notifikasi non-alarm akan ditolak, dan kamu tetap perlu Apple Developer Program berbayar untuk mendistribusikan aplikasi ke App Store.
Bisakah AlarmKit membunyikan alarm saat aplikasi ditutup total?
Bisa. AlarmKit adalah system-scheduled, jadi begitu alarm terjadwal, iOS akan memicunya tanpa perlu proses aplikasi hidup. Aplikasi akan otomatis dibangunkan (atau tetap tertidur, tergantung konfigurasi) hanya jika App Intent seperti Snooze menekan tombol yang membutuhkan logika aplikasi.
Bagaimana cara menguji AlarmKit di iOS Simulator?
Simulator iOS 26 mendukung sebagian besar API AlarmKit untuk debugging state machine, tetapi tidak memutar suara dengan reliabilitas penuh. Untuk verifikasi akhir, jalankan di device fisik. Gunakan Features > Trigger Alarm Now di menu Simulator untuk memaksa transisi ke state alerting.
Apakah AlarmKit menggantikan Local Notifications?
Tidak. AlarmKit khusus untuk alarm dan timer yang wajib berbunyi. Untuk pengingat lunak, badge, dan silent update, tetap gunakan UserNotifications. Kedua framework dirancang untuk hidup berdampingan dan bahkan bisa merujuk ke ID alarm yang sama untuk konsistensi UI.
Berapa jumlah maksimum alarm aktif per aplikasi?
Di iOS 26 beta 4, batasnya sekitar 50 alarm aktif per aplikasi. Ketika kamu mencoba menjadwalkan lebih dari itu, AlarmManager.shared.schedule akan melempar AlarmManager.Error.quotaExceeded. Gunakan Alarm.Schedule.recurring untuk pola berulang alih-alih membuat alarm terpisah per hari.
Panduan praktis NavigationStack di SwiftUI iOS 26: type-safe routing, NavigationPath, deep link parser, integrasi NavigationSplitView untuk iPad, plus catatan VoiceOver dari pengalaman production.
Typed throws di Swift 6.2 memungkinkan fungsi mendeklarasikan tipe error spesifik di signature, sehingga compiler menjamin exhaustive catch tanpa downcast. Panduan lengkap dengan sintaks, perbandingan vs Result, migrasi bertahap, dan pola production di iOS 26.
Panduan migrasi dari XCTest ke Swift Testing di iOS 26: macro @Test, #expect, #require, parameterized test, trait, plus pengalaman lapangan migrasi 480 test.