for dynamic lock screen updates
Live Activities built on ActivityKit show ongoing status information, such as a delivery status, directly on the iOS lock screen and in the Dynamic Island. This article covers the architecture, React Native integration, and the hard limits on update frequency.
Table of Contents
- 1. What Live Activities do and why ActivityKit sits behind them
- 2. Architecture: a WidgetKit extension as a technical prerequisite
- 3. Integration into a React Native app
- 4. Local updates versus push updates through ActivityKit
- 5. Practical example: live delivery status after checkout
- 6. Second example: a high-frequency live ticker
- 7. Update frequency and lifetime limits in detail
- 8. Dynamic Island presentation: compact, minimal, expanded
- 9. Limitations and testing
- 10. Summary
- 11. FAQ
1. What Live Activities do and why ActivityKit sits behind them
Live Activities are a system feature introduced with iOS 16.1 that let apps display time-bound, dynamically updating information directly on the lock screen and, on supporting hardware, in the Dynamic Island, without the user having to actively open the app. Technically, the feature is built on the ActivityKit framework, which models a running activity as a typed data object whose content can be updated repeatedly over its lifetime, while start and end are managed by the system.
The key difference from classic push notifications lies in persistence and interactivity: a Live Activity stays visibly anchored until it is explicitly ended or reaches its maximum lifetime, instead of disappearing after being tapped or dismissed like a notification. For use cases with an ongoing status, such as a delivery tracker or a live score, this is structurally the more appropriate presentation, because the user sees the current state at a glance at any time without switching apps.
2. Architecture: a WidgetKit extension as a technical prerequisite
Live Activities cannot be rendered directly from the main app, they strictly require a separate widget extension target in the Xcode project that imports both WidgetKit and ActivityKit. Inside this extension target, you define an ActivityAttributes struct that distinguishes between static data, such as an order number, and a dynamic ContentState, such as the current delivery status. Only the ContentState part can be updated during an activity's lifetime, the static attributes are fixed from the start.
The actual visual presentation is described through SwiftUI views inside the widget extension, separated for the lock screen and, if the activity should also appear there, for the three Dynamic Island states: compact, minimal, and expanded. This split means that a single business feature effectively requires four separate SwiftUI layouts to be maintained, which noticeably increases implementation effort compared to a simple in-app display.
// Shared/DeliveryActivityAttributes.swift
import ActivityKit
struct DeliveryActivityAttributes: ActivityAttributes {
public struct ContentState: Codable, Hashable {
var status: String
var estimatedMinutes: Int
var isStale: Bool
}
var orderNumber: String
}
3. Integration into a React Native app
Because Live Activities are a purely native iOS feature, no official React Native API exists for them, and a working integration strictly requires custom native Swift code or a community library such as react-native-live-activity, which already encapsulates exactly this bridge. In both cases, a native function called from JavaScript internally executes Activity.request(attributes:contentState:) and starts the activity with the initial data passed in from the JavaScript layer.
It is important that the ContentState data type must already be fixed in Swift at compile time, since it is defined as a typed struct in the widget extension. If the data model changes on the JavaScript side, say a new status field is added, the native Swift struct has to be updated in sync and the widget extension rebuilt. A purely JavaScript-driven, dynamic schema without any native change is not architecturally supported by Live Activities.
// ios/LiveActivityBridge.swift
import ActivityKit
import ExpoModulesCore
public class LiveActivityBridge: Module {
public func definition() -> ModuleDefinition {
Name("LiveActivityBridge")
Function("startDeliveryActivity") { (orderNumber: String, status: String) in
let attributes = DeliveryActivityAttributes(orderNumber: orderNumber)
let initialState = DeliveryActivityAttributes.ContentState(
status: status, estimatedMinutes: 30, isStale: false
)
_ = try Activity.request(
attributes: attributes,
content: .init(state: initialState, staleDate: nil)
)
}
}
}
4. Local updates versus push updates through ActivityKit
As long as the app is active in the foreground or briefly in the background, a running activity can be updated locally via activity.update(using:) directly from Swift code, triggered by a call from the React Native layer. For updates that need to arrive while the app is fully terminated, say because the delivery status changed on the backend, a push update through a dedicated ActivityKit push token is required instead, which you subscribe to via Activity.pushTokenUpdates and forward to your own server.
The server then sends a special APNs payload with the header apns-push-type: liveactivity and an event field, signaling either update for a new state or end for ending the activity. This push infrastructure is completely separate from an app's classic notification push token, which means a backend that already sends regular push notifications needs additional logic to manage and map Live Activity push tokens for this feature.
5. Practical example: live delivery status after checkout
An obvious use case for a Magento-connected shop is delivery tracking: once an order reaches the Shipped status, the app starts a Live Activity with the tracking number as a static attribute and the current delivery state as dynamic content. Every further status change, such as In Transit, Out for Delivery, or Delivered, arrives through a server-triggered push update, without the user ever having to reopen the app to stay informed.
In practice, this means the carrier webhook or a periodic tracking sync on the backend fires an ActivityKit push on every relevant status change, addressed to the previously stored push token for that specific activity. Once the shipment reaches Delivered, the server sends an end event, which removes the Live Activity from the lock screen automatically after a short, system-defined display duration.
// app/hooks/useDeliveryActivity.ts
import { Platform } from "react-native";
import { requireNativeModule } from "expo-modules-core";
const LiveActivityBridge = requireNativeModule("LiveActivityBridge");
export function startDeliveryActivity(orderNumber: string, status: string): void {
if (Platform.OS !== "ios") {
return;
}
LiveActivityBridge.startDeliveryActivity(orderNumber, status);
}
6. Second example: a high-frequency live ticker
A second typical use case is a live ticker, for instance for sports scores or auction countdowns, where the content changes far more frequently than in a delivery tracker. This is exactly where an important practical limit shows up: Apple caps how many push updates a Live Activity can realistically process per hour through an internal budget system, which delays or drops later push deliveries if updates arrive too frequently instead of delivering them instantly.
For a live ticker with second-by-second changes, this means in practice that updates have to be deliberately batched or reduced to relevant events, such as pushing only on goals, half-time, or significant score changes rather than transmitting every tiny data change individually. Ignoring this budget risks the Live Activity on the user's device showing noticeably stale data, even though the server has long since sent more recent push updates.
7. Update frequency and lifetime limits in detail
A Live Activity has a default maximum lifetime of eight hours from its start, after which the system ends it automatically regardless of whether the underlying task has actually finished. The staleDate field on the ContentState can additionally mark when a given piece of content is considered outdated, which the system uses to visually signal to the user that no current data is available anymore, typically through a grayed-out presentation.
After the last received push update, the system extends the activity's visibility for additional time before finally removing it, currently around four hours beyond the regular eight-hour limit, unless an explicit end event was sent. These limits are deliberately imposed by the system and cannot be worked around from the app or the server, which means any feature planning around Live Activities has to account for these hard time windows from the outset.
8. Dynamic Island presentation: compact, minimal, expanded
On devices with a Dynamic Island, starting with the iPhone 14 Pro, a Live Activity appears in three additional states beyond the lock screen presentation: compact, positioned left and right of the camera cutout during normal device use, minimal, shown as a small indicator when a second activity is running at the same time, and expanded, once the user long-presses the Dynamic Island or the device is locked. Each of these three states requires its own deliberately compact SwiftUI layout inside the DynamicIsland configuration.
The limited space in the compact view, effectively only a few dozen pixels to the left and right of the camera cutout, forces radical information reduction: for delivery tracking, often only an icon and a short time indication fit there, while the full status description is reserved for the expanded view. This design constraint is not an implementation detail, it has to be factored into the feature's business design before the first line of SwiftUI code is written.
9. Limitations and testing
Live Activities are an iOS-exclusive feature, no direct equivalent exists on Android, at best comparable to the Live Updates for ongoing notifications introduced only with Android 16, which are structured quite differently. A cross-platform React Native app therefore has to treat this functionality fundamentally as an iOS-only feature and either offer a regular notification-based alternative on Android or simply omit the feature there.
For testing, a debug build in the simulator is enough for local updates, whereas push updates strictly require a real device with a correctly configured ActivityKit push entitlement, since the simulator does not issue real APNs push tokens. Before store release, Apple explicitly checks whether a Live Activity actually shows relevant, changing information and is not being misused as a pure advertising format, a point that tends to trigger more review questions than regular notifications.
| Aspect | Live Activity (iOS) | Regular Push Notification |
|---|---|---|
| Visibility | Persistently anchored on the lock screen and Dynamic Island | Appears briefly, disappears after interaction |
| Maximum lifetime | 8 hours, up to around 12 hours with push updates | No lifetime limit, since it is a one-off event |
| Update mechanism | Local via Activity.update or ActivityKit push | Single, independent push delivery |
| Update frequency | Capped by Apple's internal budget system | Practically unlimited, but rate-limited by APNs |
| Platform | iOS 16.1 and newer only | Available on both iOS and Android |
Mironsoft
React Native app development and Magento integration
A mobile app for the Magento shop that actually runs smoothly?
We build React Native apps cleanly connected to the Magento REST or GraphQL API, from the first line of code to publishing on the App Store and Google Play.
App Concept
Plan the architecture and feature scope of a Magento-connected app together.
Magento API Integration
Cleanly connect product catalog, cart, and checkout to the shop API.
Store Publishing
Guide the App Store and Google Play release process without pitfalls.
10. Summary
iOS Live Activities: The Essentials at a Glance
Platform
iOS 16.1 and later exclusively, no direct Android equivalent.
Technical basis
A dedicated widget extension with ActivityKit and WidgetKit, no pure JS approach.
Update paths
Local via Activity.update or remote via an ActivityKit push token.
Lifetime limit
8 hours by default, up to around 12 hours with active push updates.