DEV Community

Роман
Роман

Posted on

When an iOS Live Activity expires before the baby wakes up

I build a baby tracker in Flutter. Its most used feature is a sleep timer that lives on the iPhone Lock Screen as a Live Activity, one per child. A parent starts it when the baby falls asleep, and the other parent can stop it from their own phone.

Night sleep often lasts longer than 8 hours, and 8 hours is the most ActivityKit lets a Live Activity run.

The limit

A Live Activity can stay active for 8 hours. After that the system ends it, and the ended activity can stay on the Lock Screen for up to 4 more hours. If you render elapsed time with Text(timerInterval:) or Text(date, style: .timer), the counter keeps ticking the whole time, because the system draws it, not your app.

It was worse than "the timer disappears in the morning" for two reasons:

  • The system discards update and end pushes sent to an expired activity, so the server can't clean it up afterwards.
  • We had a real case where a parent paused a night sleep, but the expired activity kept ticking on the Lock Screen for hours, until someone opened the app.

What we do now

The server reissues the activity before the limit.

  1. Track the age yourself. The server counts the activity's age from the moment its push token was registered. The device reports the token right after the start, whether the activity was started locally or by a push.
  2. Reissue at 7h45m. The server sends a push-to-start (iOS 17.2+) with the same content state. Only when APNs has accepted every start push for that device does it end the old activity with an immediate dismissal and forget its token. If a start isn't accepted, the old activity stays and the server tries again a minute later. APNs accepting the push doesn't prove the new activity appeared, which is why step 4 exists.
  3. Keep the counter continuous. The new activity renders elapsed time from the original start date, not from the moment of reissue, so for the parent the timer keeps going.
  4. Let the device clean up. When the new activity for a child shows up, the device ends the other activities for that child itself, so a duplicate from a retry doesn't stay on the Lock Screen.

A push-to-start payload looks roughly like this (sent with apns-push-type: liveactivity to the <bundle id>.push-type.liveactivity topic):

{
  "aps": {
    "timestamp": 1760160300,
    "event": "start",
    "attributes-type": "ChildPlateAttributes",
    "attributes": { "childId": "…" },
    "content-state": { "startedAt": 1760132400, "kind": "sleep" },
    "alert": { "title": "Sleep", "body": "Since 22:15" }
  }
}
Enter fullscreen mode Exit fullscreen mode

Trade-offs we accepted

  • A push-to-start must carry an alert, so every reissue lights up the screen. We show the original start time ("since 22:15") instead of "timer started", so it doesn't read like a new event.
  • Quiet hours take priority over the reissue. If the limit falls inside the parent's quiet hours, the server only ends the old activity and starts a new one when quiet hours are over. Night sleep stays without a Lock Screen timer for that gap.
  • The age can be underestimated. If the token arrives late (for example, the app was force-quit and the activity was started by a push), the activity may expire before the reissue. Then the app restarts it the next time it opens.
  • A swipe while the app is suspended is invisible to the server, so such an activity can come back at the reissue.
  • Below iOS 17.2 there is no push-to-start, so the activity lives until the limit and the app restarts it on open.

Android, for comparison

On Android the same timer is a notification with a system Chronometer. The app sends no per-second updates: the system draws the time, so it keeps counting even when the process is frozen.

If you are building something similar

Keep the start date as the source of truth and compute everything from it, not from the last update. A reissue can only continue the counter if it knows when the sleep began. And without a server you can't reissue reliably, because the app is usually not running when the 8 hours run out.

I work on Lulumi, a baby tracker. The Android app is on Google Play, and the iPhone version, which has the Lock Screen timer from this post, is in App Store review. More at lulumiapp.com.

Top comments (0)