Build

My app kept rescheduling alarms that were already ringing

Returning to the foreground made RYZA re-sync its alarm schedule, including the one alerting on the lock screen at that moment. Repair work is still work.

I found this one in an ordinary moment. I dismissed the alarm screen, opened the app again, and the alarm behaved as if it had changed its mind.

The phone was still in an active alarm state and AlarmKitApple’s system framework for alarms, which owns the alert on the lock screen knew that. But when RYZA came back to the foreground, the app started its usual “make sure every enabled alarm is scheduled correctly” work.

That sounds harmless. It was not.

An active alarm is a different thing from an upcoming alarm. An upcoming alarm can be cancelled and recreated without anyone noticing. An active alarm is already in someone’s hands, usually on the lock screen, usually before coffee. Touching it at the wrong moment breaks the chain between the system alert, the voice mission, and the app’s own saved state.

It did not fail dramatically every time, which is the worst kind. Sometimes the alarm carried on. Sometimes the mission opened late. Sometimes the app came back looking completely normal while the system-level alarm was still trying to finish something else.

That inconsistency is impossible to explain to someone, because from their side there is only one alarm.

Two screens, one alarm

RYZA has two places an alarm can be visible.

The lock screen, where iOS and AlarmKit own the alert. And the app, where Flutter shows the voice mission, the settings, and the result screens.

Those two do not wake at the same time or follow the same rules.

Alarm rings on lock screen

User taps an action

iOS records the action

RYZA returns to the foreground

App reads the action and opens the right screen

Clean in a diagram. Real life added interruptions at every arrow.

Someone could tap a lock-screen action, switch to the app, lock the phone again, then come back. The app could resume while an AlarmKit alert was still active. The same action could arrive more than once. An old handoff could show up after a newer one.

My first version of the resume flow was too eager. When the app became active it synced enabled alarms, with a perfectly good goal: keep the native schedule and the saved list aligned.

But a sync means cancelling, recreating, or updating native alarms. Which is exactly the wrong thing to do to an alarm that is currently alerting.

I had written a repair routine with no sense of occasion.

The fix was a question, asked before the sync

The change was small.

Before the app synchronises its enabled alarms after returning to the foreground, it asks AlarmKit whether any alarm is currently alerting. If the answer is yes, the sync stops there.

Not forever. Only until the active alert has finished its own path. The app no longer tidies up an alarm while someone is still looking at it on the lock screen.

App resumes Is an AlarmKit alert active? YES reschedule nothing NO run the normal sync
An alerting alarm outranks routine maintenance. No clever exception buried inside the scheduling code.

I like this rule because it is easy to inspect later. It was also a change in attitude. I had been thinking of rescheduling as maintenance, and in an alarm app maintenance is an action. An action at the wrong moment changes the experience.

The app is not where an action first appears

The lock screen can receive an alarm action before Flutter is fully awake, so iOS needs a way to hold that action until the app is ready.

The native side now stores platform action events. When the app returns it reads them, applies them, and acknowledges them only after the app-side work has actually succeeded.

That last part is the whole thing.

My instinct was to treat the event like a notification: read it once, throw it away. But a notification can arrive while the app is busy, half-launched, or about to be interrupted again. Erase it too early and there is nothing left to recover from.

So the event behaves more like a note left on a desk.

  • iOS writes it down.
  • The app picks it up.
  • The app finishes the relevant work.
  • Only then is the note removed.

I also made repeated delivery safe, because the system can replay an action after a restart or during a state change. I do not need every replay to disappear. I need the same replay to have no harmful second effect, which meant identifying actions and tracking work that had already completed.

“Dismissed” and “finished” are not the same word

That distinction caused more confusion than anything else here.

On the lock screen, dismissing an alert can mean the sound has stopped. It does not always mean the voice mission is complete. For a standard alarm those are the same event. For a voice mission, dismissal is the moment the app should open the next step.

I had a version where the two meanings sat too close together. A system action would remove the alert while the app treated the whole alarm occurrence as finished, which left almost no room to recover if the mission handoff was delayed.

The current flow keeps three things separate: the alarm occurrence, the platform schedule ID, and the mission handoff. It also rejects handoffs that are stale or do not match the alarm it currently knows about.

I did not want a very old lock-screen action opening a fresh voice mission later in the day. That sounds like a rare edge case right up until it happens once.

What I had assumed

That returning to the app was a safe moment to reconcile everything.

Reasonable for a normal settings change. False for an active alarm.

The scheduler was not broken. It was doing the job I gave it: look at enabled alarms, make the native schedule match. It had no idea that one of those schedules was currently in use as an alert.

The fix was not a rewrite. It was mostly a boundary.

If iOS says an alarm is alerting, the resume path does not reschedule that alarm family.

Some of this I still cannot control. iOS decides when processes wake and when system actions arrive, and I cannot force those timings into a tidy sequence. This is the third time the two layers have disagreed with each other, after the stop button and a reboot leaving the app unable to read its own data. I expect a fourth.

What I can control is whether RYZA treats an active alarm as disposable state. It does not anymore.

For a user the result should be boring. The alarm rings, the lock-screen action does what it says, and opening the app does not quietly rewrite the state underneath it.

That is the kind of boring I want at 7:03 in the morning.