◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SONNET 4.5 — Retires on the Claude API on 11/30, 27 days left. If your app calls it, check the migration target●SDK 58 — Moves to React Native 0.88. R8 is now on by default in Android release builds, and iOS prebuild generates a SceneDelegate●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SONNET 4.5 — Retires on the Claude API on 11/30, 27 days left. If your app calls it, check the migration target●SDK 58 — Moves to React Native 0.88. R8 is now on by default in Android release builds, and iOS prebuild generates a SceneDelegate●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
Articles/Dev Tools
⬡ Dev Tools/2026-06-14Advanced

Updating Live Activities Remotely: Putting Live Lock Screen Info on a Rork App

A practical design for updating Live Activities remotely through APNs so the Lock Screen and Dynamic Island stay current even when your app is closed. Covers push-to-start vs update tokens, the content-state payload, stale-date and the update budget, and bridging from Expo, with working code and the issues I hit in production.

Live Activities6ActivityKit4APNs3Expo234Rork Max235iOS118

✦ Premium Article

When I decided to show wallpaper download progress on the Lock Screen, the first thing that tripped me up was a single question: while the app is closed, who actually updates this display? A Live Activity keeps running when your app is in the background, but if the app itself tries to drive the updates, it runs straight into background execution limits.

The answer was to build a path that updates the Live Activity directly from the server over APNs. Get this design wrong and a stale number stays glued to the Lock Screen forever. The approach is the same whether you ship an Expo app generated by Rork or a native Swift app generated by Rork Max. Here is what I learned, in the order it caused trouble.

There are two update paths

A Live Activity can be updated in two ways: locally and remotely. Local updates call Activity.update(...) during the short window your app is in the foreground. They are immediate, but they stop the moment the app sleeps.

Remote updates push to APNs, and the system rewrites the display when it receives them. Because they arrive even when the app is closed, anything that "moves forward outside the app" — progress, scores, a courier's position — needs this path.

Running several apps solo as an indie developer, I settled on shaping the initial display locally for the first few seconds, then sending every continuing update remotely. Mixing the two halfway makes it impossible to know which value is authoritative.

Two tokens: push-to-start and update

The first thing to grasp about remote updates is that there are two kinds of token. One is the push-to-start token, used to start a Live Activity that does not yet exist from the server side. The other is the update token, used to update one specific Activity that is already running.

The push-to-start token is a single token for the whole app, and you subscribe to it at launch. The update token is issued per Activity and arrives asynchronously right after you start one. Confuse the order and you end up able to start an Activity but never deliver a single update afterward.

import ActivityKit
 
@available(iOS 17.2, *)
func registerPushToStart() {
    Task {
        for await data in Activity<DownloadAttributes>.pushToStartTokenUpdates {
            let token = data.map { String(format: "%02x", $0) }.joined()
            // One per app. Register "this user can accept a fresh start" with the server
            await sendToServer(kind: "start", token: token, activityId: nil)
        }
    }
}
 
@available(iOS 16.1, *)
func observeUpdateToken(for activity: Activity<DownloadAttributes>) {
    Task {
        for await tokenData in activity.pushTokenUpdates {
            let token = tokenData.map { String(format: "%02x", $0) }.joined()
            // Issued per Activity, and it expires. Overwrite on the server every time
            await sendToServer(kind: "update", token: token, activityId: activity.id)
        }
    }
}

The key detail is that pushTokenUpdates is not a one-shot fetch but a stream that emits more than once. Tokens rotate, so if you save the first one and call it done, updates quietly stop a few hours later. Keep the subscription alive and overwrite the server each time one arrives.

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦Follow which of the two tokens (push-to-start vs update) to fetch in what order, and where to send each to your server, in working Swift and TypeScript
✦Take home an APNs payload design with stale-date, dismissal-date and relevance-score, plus a sense of the numbers that keep you inside the update budget
✦Understand the three production traps (a frozen Lock Screen, a broken Dynamic Island, an expired token) and the exact way around each
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

Related Articles

⬡ Dev Tools2026-04-05
Rork Max × WidgetKit & Live Activities in Practice — From Home Screen to Dynamic Island
A complete guide to implementing iOS Widgets, Lock Screen widgets, and Dynamic Island Live Activities with Rork Max. Covers WidgetKit fundamentals, Timeline update strategies, App Intents integration, and monetization.
⬡ Dev Tools2026-10-30
It runs in development but the iOS Release build dies on launch — checking whether a missing scheme is the cause
In an expo-router app, an app.json with no scheme can crash only the iOS Release build at launch. Why Debug and Android survive, the two ways to fix it, how to think about leaving the scheme out on purpose, and a pre-submission check.
⬡ Dev Tools2026-09-21
A build that stops with exit code 0 — the flag that silenced the log, and the check that let an empty file through
Two reports landed in the same week: eas build --local stopping with exit code 0 but produced no further output, and a zero-byte stub that never got embedded. Both came down to settings that reduce output. Here is how to audit your own scripts.