Migrating from UserNotifications to AlarmKit in iOS 26
A Long time ago I built an assistant app for the online game ‘Gersang’.
The first feature was notifications for daily events.
The game hosts daily events, such as stamps, test wars.
Sometimes I forgot to attend the events, so I decided to build a feature to notify users about them.
I implemented the feature with local notifications.
I added notification sounds using the recorded background music, converted to .aiff format.
Users could ignore or remove the notifications, but I couldn’t make alarms like the built-in Clock app.
I reused my code for this feature at the company and in my Study app, which remind users of their study time. For years, I didn’t touch the feature and eventually forgot about it.
I was reminded of the AlarmKit at a conference ‘Let’Swift’.
Now that the new framework for alarms has been introduced in iOS 26, I decided to migrate AlarmKit for iOS 26+, event though it is only available in iOS 26 and later.
Framework
Local Notification
I used UserNotifications framework to send local notifications.
Alarm
Alarms can only be controlled by AlarmKit.
Authorization
UserNotifications
Both frameworks require permission. For local notifications, I had to call requestAuthorization(options: completionHandler:).
UNUserNotificationCenter.current().requestAuthorization(options: )The app crashed without a usage description in info.plist.
AlarmKit
The Alarm also need permission, but the requestAuthorization() method doesn’t accept any options.
AlarmManager.shared.requestAuthorization()I added the required usage description for AlarmKit in Info.plist
Content
UserNotifications
I could specify the content of local notification using UNNotificationContent, but I had to use UNMutableNotificationContent to create one.
I defined the description’s title and body as follows:
let content = UNMutableNotificationContent()
content.title = "Study Alarm";
content.body = "Time to Study";This sent me this notification:
AlarmKit
Alarms support only the title, I can define the alarm screen using AlarmPresentation.Alert.
AlarmPresentation.Alert(
title: "Study Time",
stopButton: ...,
)This initializer requires stopButton, but it has been deprecated in iOS 26.1 .
AlarmPresentation.Alert(
title: "Study Time"
}I can see the alarm in lock screen:
The Alert class doesn’t provide a way to control colors, but I can specify it using AlarmConfiguration. I‘ wi’ll explore this in more detail later.
let configuration = AlarmManager.AlarmConfiguration(
...,
attributes: .init(
...,
tintColor: .yellow
),
)The button appears with a tinted background.
Date
UserNotifications
To schedule local notifications, I created triggers with the target date using UNCalendarNotificationTrigger. It takes DataComponents, not Date.
let dateComponents = Calendar.current.dateComponents([.day,.minute,.hour, .month,.year,.second], from: date);
let trigger = UNCalendarNotificationTrigger.init(dateMatching: dateComponents, repeats: false);AlarmKit
Fortunately, alarms can use Date directly.
let schedule = Alarm.Schedule.fixed(date)Time
UserNotifications
Time scheduling for local notifications was the same way as date scheduling.
let trigger = UNCalendarNotificationTrigger.init(dateMatching: timeInfo, repeats: false);AlarmKit
However, the alarm provides entirely different method. I need to create a relative schedule using Time.
let time = Alarm.Schedule.Relative.Time(
hour: timeComponents.hour ?? 0,
minute: timeComponents.minute ?? 0
)
let schedule = Alarm.Schedule.relative(
Alarm.Schedule.Relative(
time: time,
repeats: .never
)
)Weekday
UserNotifications
The events occurs daily, so I had to send notifications every day. But I couldn’t attent all events due to my work schedule.
So I had to schedule notifications for specific weekdays.
However, UserNotifications doesn’t support setting multiple weekdays in a single notification.
As a result, I had to make notifications
dateComponents.weekDay = day; // Sunday: 1for each weekday.
let weekdays : [DateComponents.DateWeekDay?] = notification.weekdays?.days.map({ (day) -> DateComponents.DateWeekDay? in
return day;
}) ?? [nil];AlarmKit
An alarm can specify weekdays directly, no more boilerplate schedules needed. With the Recurrence type set to weekly, I can specify on which weekday to trigger the alarm.
let localeWeekdays: [Locale.Weekday] = [.sunday, ...]
let schedule = Alarm.Schedule.relative(
Alarm.Schedule.Relative(
time: time,
repeats: .weekly(localeWeekdays)
)
)Register
UserNotifications
I scheduled notifications through UNUserNotificationCenter using UNNotificationRequest.
let req = UNNotificationRequest.init(identifier: "Notification ID",
content: content,
trigger: trigger);
try await UNUserNotificationCenter.current().add(req)I had to create multiple requests for the weekdays with the following identifiers:
- “{Notification ID}Sun”
- “{Notification ID}Mon” …
AlarmKit
AlarmConfirguration takes over the Request’s role, and the Center is replaced with AlarmManager.
let configuration = AlarmKit.AlarmManager.AlarmConfiguration(
schedule: schedule,
attributes: .init(
presentation: .init(alert: alertContent),
metadata: metadata,
tintColor: Color(red: 0.506, green: 0.831, blue: 0.980)
),
)
try await AlarmKit.AlarmManager.shared.schedule(id: "Alarm ID",
configuration: configuration)Action
UserNotifications
While many users don’t know this, we can give feedbacks to the notification. I could create the interactive actions by setting UNNotificationCategory.
let category = UNNotificationCategory(identifier: identifier,
actions: [UNNotificationAction.init(
identifier: "Confirm Action ID",
title: "Confirm",
options: .destructive),
UNNotificationAction.init(
identifier: "Study Action ID",
title: "Study",
options: .foreground)],
intentIdentifiers: [],
options: .customDismissAction);
UNUserNotificationCenter.current().setNotificationCategories(Set([category]));We can see these actions only by long-pressing the notification,
I handled the action using events from UNUserNotificationCenterDelegate.
class UserNotificationManager : UNUserNotificationCenterDelegate {
init(){
...
UNUserNotificationCenter.current().delegate = self;
}
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
let categoryIdentifier = response.notification.request.content.categoryIdentifier
let categories = await UNUserNotificationCenter.current().notificationCategories()
guard let category = categories.first(where: { $0.identifier == categoryIdentifier }) else {
return
}
switch response.actionIdentifier {
case "Confirm Action ID": ...
case "Study Action ID": ...
}
}
}AlarmKit
With alarms, I can add action buttons in the Alert without any additional classes.
let alertContent = AlarmKit.AlarmPresentation.Alert(
title: "Study",
stopButton: AlarmButton(
text: "Confirm",
textColor: .white,
systemImageName: "checkmark.circle"
),
secondaryButton: AlarmButton(
text: "Study",
textColor: .black,
systemImageName: "book.fill"
),
secondaryButtonBehavior: .custom
)However, when I specified secondaryButton without secondayButtonBehavior or .none, I encountered this error:
Error Domain=com.apple.AlarmKit.Alarm Code=0 “(null)”
secondayButtonBehavior can have one of these values:
- .none: secondayButtonBehavior is optional, so use can
.none, but it is not a case value. - .custom: Performs a custom intent.
- .countdown: Postpone the alarm by countdownDuration. (I’ll cover this in another section later)
I couldn’t see the stop button, because iOS 26.1 changed the stop button a slider.
To see the stop button again, enable Prefer Single Touch Actions from Settings.
With this setting, Stop button returns,
But I still can’t customize, it because stopButton is deprecated, as I mentioned earlier.
Despite all these settings, I couldn’t trigger the alarm from the unlocked screen. Because Mute? Only haptic feedback occured once. I am still not sure why..
If you chose .custom for the secondary button behavior, you should specify the intents in AlarmConfiguration,
let configuration = AlarmKit.AlarmManager.AlarmConfiguration(
...,
stopIntent: StopStudyAlarmIntent(alarmID: alarmID.uuidString),
secondaryIntent: OpenStudyAppIntent(alarmID: alarmID.uuidString)
)which defines perform method as follows:
struct OpenStudyAppIntent: LiveActivityIntent {
func perform() throws -> some IntentResult {
// Stop the alarm
try AlarmKit.AlarmManager.shared.countdown(id: UUID(uuidString: alarmID)!)//.stop(id: UUID(uuidString: alarmID)!)
return .result()
}
...
}To open the app using button in the lock screen, we need to add openAppWhenRun.
struct OpenStudyAppIntent: LiveActivityIntent {
static var openAppWhenRun = true- Note: openAppWhenRun doesn’t work from widgets no longer.
I was curious why it is static, but it was deprecated in iOS 26.0.
Appled introduced supportedModes instead. .foreground(.immediate) results in the same behavior as setting openAppWhenRun to true.
static var supportedModes: IntentModes {
.foreground(.immediate)
}Sound
UserNotifications
Nowadays we received too many notifications throughout the day. So I thought my notification could grab users’ attention with a special sound.
Get Lee young-jun’s stories in your inbox
Join Medium for free to get updates from this writer.
I recorded or extracted the event sounds and converted them to .aiff (this is only allowed format).
I set UNNotificationSound to the ‘sound’ property of the notification using this file.
content.sound = UNNotificationSound(named: .init(rawValue: "Ba_Win.aiff"))AlarmKit
Fortunately, AlarmKit supports mp3 files, but .aiff doesn’t work. I’m not sure if the issue was the track length.
let configuration = AlarmKit.AlarmManager.AlarmConfiguration(
...,
sound: AlertSound.named("your sound file name with the extension") // file.mp3
}Repeat
UserNotifications
To repeat the notification in a specific time and weekday, the trigger’s repeats property should be true.
let trigger = UNCalendarNotificationTrigger.init(dateMatching: dateComponents,
repeats: true);AlarmKit
However, the relative schedule’s repeats property means the schedule itself repeats. No additional properties are required.
let schedule = Alarm.Schedule.relative(
Alarm.Schedule.Relative(
time: time,
repeats: .weekly(...)
)
)Countdown
UserNotifications
There wasn’t a way to notify the users in advance.
AlarmKit
If you’ve used Clock app, you know that a countdown is displayed on the lock screen until the alarm fires.
I wanted to alert the user before their the study time arrived, so I set countdownDuration.
let configuration = AlarmKit.AlarmManager.AlarmConfiguration(
countdownDuration: .init(preAlert: 60, postAlert: 60),It is a type of Alarm.CountdownDuration and has these properties:
- preAlert: Duration for widget display before the alarm fires.
- postAlert: Duration for widget display after the first alarm.
Live Activity
According to WWDC2025 video, countdown requires a Live Activity:
There is no types of the target named “Live Activity”.
We should use the widget extension for Live Activity.
I referred the sample project to set up my Live Activity.
Don’t forget to add a class derived from MetaData into the App target to share it.
The widget won’t work if it is not linked to the app target.
lock screen
The first closure of ActivityConfiguration, content , defines the view on the lock screen:
struct AlarmLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: AlarmAttributes<YourMetaData>.self) { context in
lockScreenView(attributes: context.attributes, state: context.state)
}The context parameter has a state property of type AlarmsPresentationState.
func lockScreenView(attributes: AlarmAttributes<StudyAlarmMetadata>, state: AlarmPresentationState) -> some View I can identify the alarm through the state.
StopIntent(alarmID: state.alarmID.uuidString)The Intents use the provided uuid to control the alarm.
struct StopIntent: LiveActivityIntent {
func perform() throws -> some IntentResult {
try AlarmKit.AlarmManager.shared.stop(id: UUID(uuidString: alarmID)!)
return .result()
}If the countdown duration is configured as follows:
countdownDuration: .init(preAlert: 10The widget on the lock screen will look like this:
DynamicIsland
If the device is not locked, the widget reminds us the remain time in the status bar. This is called DynamicIsland and can be defined via dynamicIsland parameter of ActivityConfiguration.
ActivityConfiguration(for: AlarmAttributes<YourMetaData>.self) { context in
...
} dynamicIsland: { context in
DynamicIsland { ... }
}There are many types of Dynamic Island presentations:
compactLeading
compactLeading is the left content of the Dynamic Island:
I can determine if the alarm is countdown mode through AlarmPresentationState.Mode and obtain fireDate.
compactLeading: {
// The compact leading presentation.
countdown(state: context.state, maxWidth: 44)
.foregroundStyle(context.attributes.tintColor)
}
...
func countdown(state: AlarmPresentationState, maxWidth: CGFloat = .infinity) -> some View {
Group {
switch state.mode {
case .countdown(let countdown):
Text(timerInterval: Date.now ... countdown.fireDate, countsDown: true)compactTrailing
compactTrailing is for the opposite position:
Implementation is same as for compactLeading.
compactTrailing: {
// The compact trailing presentation.
AlarmProgressView(mode: context.state.mode,
tint: context.attributes.tintColor)
}Expanded
The Live Activity expanded when long-pressed, which is called Expanded:
DynamicIsland {
// The expanded Dynamic Island presentation.
DynamicIslandExpandedRegion(.leading) {
alarmTitle(attributes: context.attributes, state: context.state)
}
DynamicIslandExpandedRegion(.trailing) {
studySubtitle(metadata: context.attributes.metadata)
}
DynamicIslandExpandedRegion(.bottom) {
bottomView(attributes: context.attributes, state: context.state)
}
}The Dynamic Island expands to the full width:
Minimal
If another Live Activity take priority (such as Clock app’s alarm), my app’s timer will be minimized, just showing the progress:
To define this minimized widget, I can specify minimal presentation:
minimal: {
AlarmProgressView(mode: context.state.mode,
tint: context.attributes.tintColor)
}Button decoration
We saw AlarmButton configuration earlier in the AlarmConfirguation initialization.
let countdownContent = AlarmKit.AlarmPresentation.Countdown(
title: "Pause",
pauseButton: AlarmKit.AlarmButton(
text: "Pause",
textColor: .white,
systemImageName: "pause"
)
)Through the configuration from the app,
let configuration = AlarmKit.AlarmManager.AlarmConfiguration(
...,
attributes: .init(
presentation: .init(
...,
countdown: countdownContent,the widget receives the AlarmAttributes:
DynamicIslandExpandedRegion(.bottom) {
bottomView(attributes: context.attributes, state: context.state)
}The AlarmAttributes provides AlarmPresentationState:
func bottomView(attributes: AlarmAttributes<StudyAlarmMetadata>, state: AlarmPresentationState) -> some View {
HStack {
...
AlarmControls(presentation: attributes.presentation, state: state)
}
}Finally we can use the button configuration
struct AlarmControls: View {
var presentation: AlarmPresentation
...
ButtonView(config: presentation.countdown?.pauseButtonto create the buttons:
struct ButtonView<I>: View where I: AppIntent {
var config: AlarmButton
...
Label(config.text, systemImage: config.systemImageName)I can see the result on the lock screen:
There’s also stopButton, but it has been deprecated.
Clear
UserNotifications
I removed the notifications using UNUserNotificationCenter to sync with user’s settings or to clear notifications.
I called removeDeliveredNotifications to remove the specific notifications from the notification center.
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: noti_ids);Pending notifications that haven’t not fired yet can be removed using removePendingNotificationRequests.
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: noti_ids);I could clear all notifications using removeAll methods.
UNUserNotificationCenter.current().removeAllDeliveredNotifications();
UNUserNotificationCenter.current().removeAllPendingNotificationRequests();AlarmKit
However, AlarmKit doesn’t provide a method to clear all alarms. It only provides a way to cancel a single alarm.
AlarmManager.shared.cancel(id: alarmID)There is no cancelAll method available.
Final thought
These days, the users are flooded with the notifications. So your ciritical reminder service might be ignored if it’s just another notification.
With this special, official alarm framework, ensure the user can’t miss important schedules.
However, iOS 26.1 introduces some challenges, as it deprecated several properties.
Always test on the real devices before releasing to users.
You’ll need to create a widget extension if you want to display countdown timers.
Happy Alarming! 😊
If you found this post helpful, please give it a round of applause 👏 and buy me a coffee ☕️. Explore more iOS-related content in my other posts.
For additional insights and updates, check out my LinkedIn profile. Thank you for your support!
Troubleshooting Guides
iOS Team Provisioning Profile: *” doesn’t include the Push Notifications capability
Create settings(.xcconfig) for Widget extension, if you don’t need to receive notifications.
Error Domain=com.apple.AlarmKit.Alarm Code=0 “(null)”
Set secondayButtonBehavior, if you specified secondayButton









