Sitemap

Migrating from UserNotifications to AlarmKit in iOS 26

11 min readJan 21, 2026

--

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.

Press enter or click to view image in full size

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: 1

for 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.

Press enter or click to view image in full size

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

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”.

Press enter or click to view image in full size

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: 10

The 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?.pauseButton

to 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.

Press enter or click to view image in full size

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

References

--

--