
Live Activity Generator
- 3 installs
- 591 repo stars
- Updated July 24, 2026
- rshankras/claude-code-apple-skills
Generates ActivityKit Live Activity infrastructure with Dynamic Island layouts, Lock Screen presentation, lifecycle management, and push-to-update support.
About
Generates a complete ActivityKit Live Activity implementation with Dynamic Island layouts, Lock Screen presentation, and push-to-update support. A developer uses it to show ongoing real-time status like delivery, timers, or scores on iOS.
- Dynamic Island and Lock Screen layouts
- Push-to-update activity lifecycle management
Live Activity Generator by the numbers
- 3 all-time installs (skills.sh)
- +1 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #887 of 1,039 Mobile Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/rshankras/claude-code-apple-skills --skill live-activity-generatorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 3 |
|---|---|
| repo stars | ★ 591 |
| Last updated | July 24, 2026 |
| Repository | rshankras/claude-code-apple-skills ↗ |
What it does
Generates ActivityKit Live Activity infrastructure with Dynamic Island layouts, Lock Screen presentation, lifecycle management, and push-to-update support.
Files
Live Activity Generator
Generate a complete ActivityKit Live Activity implementation with Dynamic Island layouts, Lock Screen presentation, activity lifecycle management, and push-to-update support.
When This Skill Activates
Use this skill when the user:
- Asks to "add Live Activities" or "add a Live Activity"
- Mentions "Dynamic Island" layouts
- Wants real-time status updates on the Lock Screen
- Asks about "ActivityKit" or "ActivityAttributes"
- Mentions "push-to-update" for live activities
- Wants to show ongoing progress (delivery tracking, sports scores, timers, workouts)
Pre-Generation Checks
1. Project Context Detection
- [ ] Check for existing ActivityKit implementations
- [ ] Check for an existing widget extension target
- [ ] Verify deployment target is iOS 16.1+
- [ ] Identify source file locations
2. Conflict Detection
Search for existing Live Activity code:
Glob: **/*Activity*.swift, **/*LiveActivity*.swift
Grep: "ActivityKit" or "ActivityAttributes" or "ActivityConfiguration"If an existing widget extension is found:
- Ask if the Live Activity should be added to the existing widget bundle
- Check for existing
WidgetBundleto extend
If Live Activity code already exists:
- Ask user whether to replace or extend
3. Required Capabilities
Live Activities require:
- iOS 16.1+ deployment target
NSSupportsLiveActivitiesset toYESin Info.plist- A widget extension target (can share with existing WidgetKit widgets)
- For push updates:
NSSupportsLiveActivitiesFrequentUpdates(optional, iOS 16.2+)
Configuration Questions
Ask user via AskUserQuestion:
1. What is the Live Activity for? (freeform)
- Examples: delivery tracking, sports score, workout timer, ride-sharing, food order
- This determines the attribute fields and content state
2. What data should appear on the Lock Screen? (freeform)
- Static data (set at start, does not change): e.g., restaurant name, order number
- Dynamic data (updated over time): e.g., ETA, driver location, score
3. Dynamic Island layout needs?
- Compact (leading + trailing) only
- Expanded (leading, trailing, center, bottom) only
- Both compact and expanded (recommended)
4. Push-to-update support?
- Yes -- server can update the activity remotely via APNs
- No -- updates only from the app process
5. Alert configuration when ending?
- Yes -- show a final alert on the Lock Screen when the activity ends
- No -- dismiss silently
Generation Process
Step 1: Determine File Locations
Check project structure:
- If a widget extension target exists, add files there
- Otherwise, instruct user to create a Widget Extension target first
For the activity manager (lives in the main app target):
- If
Sources/exists -->Sources/LiveActivity/ - If
App/exists -->App/LiveActivity/ - Otherwise -->
LiveActivity/
For the widget extension views:
- Place inside the existing widget extension directory (e.g.,
MyAppWidgets/)
Step 2: Create Core Files
Generate these files based on configuration answers:
1. `{Name}ActivityAttributes.swift` -- Shared between app and widget extension
ActivityAttributesstruct with static properties- Nested
ContentStatewith dynamic properties
2. `{Name}LiveActivityView.swift` -- Widget extension file
ActivityConfigurationwith Lock Screen, Dynamic Island layouts
3. `{Name}ActivityManager.swift` -- Main app target file
- Protocol-based manager for start, update, end, and push token handling
Step 3: Generate Code from Templates
Use the templates in templates.md and customize based on user answers:
- Replace placeholder attribute names with real fields
- Configure Dynamic Island regions based on layout choice
- Include or exclude push-to-update code
- Include or exclude alert configuration
Info.plist Required
Main App Target
<key>NSSupportsLiveActivities</key>
<true/>
<!-- Optional: for frequent push updates (iOS 16.2+) -->
<key>NSSupportsLiveActivitiesFrequentUpdates</key>
<true/>Output Format
After generation, provide:
Files Created
Sources/LiveActivity/
├── {Name}ActivityAttributes.swift # Shared: attributes + content state
└── {Name}ActivityManager.swift # Main app: lifecycle management
MyAppWidgets/
├── {Name}LiveActivityView.swift # Widget ext: all Live Activity UI
└── (update WidgetBundle if needed)Integration Steps
1. Add the widget extension target (if not present):
- File > New > Target > Widget Extension
- Ensure "Include Live Activity" is checked
2. Share the attributes file between targets: The {Name}ActivityAttributes.swift file must be included in both the main app target and the widget extension target. Select the file in Xcode, open the File Inspector, and check both targets under "Target Membership".
3. Add Info.plist entries in the main app target:
<key>NSSupportsLiveActivities</key>
<true/>4. Register the Live Activity configuration in the widget bundle:
@main
struct MyAppWidgets: WidgetBundle {
var body: some Widget {
// Existing widgets...
MyAppLiveActivity()
}
}5. Start a Live Activity from your app:
let attributes = DeliveryActivityAttributes(
orderNumber: "1234",
restaurantName: "Pizza Place"
)
let initialState = DeliveryActivityAttributes.ContentState(
status: .preparing,
estimatedDelivery: Date().addingTimeInterval(30 * 60),
driverName: nil
)
let manager = LiveActivityManager()
try await manager.startActivity(attributes: attributes, state: initialState)6. Update the activity when state changes:
let updatedState = DeliveryActivityAttributes.ContentState(
status: .enRoute,
estimatedDelivery: Date().addingTimeInterval(15 * 60),
driverName: "Alex"
)
await manager.updateActivity(state: updatedState)7. End the activity when complete:
let finalState = DeliveryActivityAttributes.ContentState(
status: .delivered,
estimatedDelivery: Date(),
driverName: "Alex"
)
await manager.endActivity(state: finalState, dismissalPolicy: .default)Push-to-Update Setup (if enabled)
1. Observe push tokens: The generated {Name}ActivityManager automatically observes push token updates. Send the token to your server when it changes.
2. Server-side APNs request:
curl -v \
--header "authorization: bearer $JWT_TOKEN" \
--header "apns-topic: com.yourcompany.yourapp.push-type.liveactivity" \
--header "apns-push-type: liveactivity" \
--http2 \
--data '{"aps":{"timestamp":1234567890,"event":"update","content-state":{"status":"enRoute","estimatedDelivery":1234568000,"driverName":"Alex"}}}' \
https://api.push.apple.com/3/device/$PUSH_TOKENNote: The apns-topic must be your app bundle ID with .push-type.liveactivity appended.
3. Ending via push:
{
"aps": {
"timestamp": 1234567890,
"event": "end",
"dismissal-date": 1234568000,
"content-state": {
"status": "delivered",
"estimatedDelivery": 1234567890,
"driverName": "Alex"
}
}
}Testing Instructions
1. Device required: Live Activities and Dynamic Island cannot be tested in Simulator (as of Xcode 15). Use a physical device with iOS 16.1+. 2. Dynamic Island: Requires iPhone 14 Pro or later (devices with the Dynamic Island hardware). 3. Push-to-update testing: Use a tool like apnspush or curl with an APNs auth key (.p8 file) to send test payloads. 4. Debug tips:
- Use
Activity<Attributes>.activitiesto list all running activities - Check Console.app for ActivityKit logs filtered by your app bundle ID
- Activities are limited to a maximum of 5 concurrent per app
Common Patterns
Delivery Tracking
Static: order number, restaurant name Dynamic: status enum, ETA, driver name
Sports Score
Static: home team, away team, sport type Dynamic: home score, away score, period/quarter, game clock
Workout / Timer
Static: workout type, goal Dynamic: elapsed time, calories, heart rate, distance
Ride-Sharing
Static: pickup location, destination Dynamic: driver name, ETA, vehicle info, status
Gotchas and Limits
- Size limit:
ActivityAttributes+ContentStatecombined must be under 4KB - Concurrent limit: Maximum 5 active Live Activities per app
- Duration: Activities automatically end after 8 hours (can remain on Lock Screen for up to 4 more hours in ended state)
- Push token changes: The push token can change during the lifetime of an activity; always observe
pushTokenUpdates - Stale state: Always set
staleDateon content to let the system show stale indicators - Background updates: The app can update activities from the background, but not start new ones
References
- templates.md -- Code templates for attributes, widget views, and manager
- ActivityKit Documentation
- Displaying live data with Live Activities
- Updating and ending your Live Activity with ActivityKit push notifications
Live Activity Code Templates
Production-ready Swift templates for ActivityKit Live Activities. All code targets iOS 16.1+ and uses modern Swift concurrency.
ActivityAttributes Definition
The attributes struct is shared between the main app target and the widget extension target.
import ActivityKit
import Foundation
/// Defines the static and dynamic data for the Live Activity.
///
/// - Static properties (on the struct itself): Set once when the activity starts.
/// - Dynamic properties (in ContentState): Updated throughout the activity lifetime.
struct DeliveryActivityAttributes: ActivityAttributes {
// MARK: - Content State (Dynamic)
/// The dynamic data that changes over the lifetime of the Live Activity.
struct ContentState: Codable, Hashable {
/// Current delivery status.
var status: DeliveryStatus
/// Estimated delivery time.
var estimatedDelivery: Date
/// Name of the delivery driver (nil until assigned).
var driverName: String?
}
// MARK: - Static Properties
/// The order number displayed throughout the activity.
var orderNumber: String
/// The name of the restaurant.
var restaurantName: String
}
// MARK: - Status Enum
/// Represents the stages of a delivery.
enum DeliveryStatus: String, Codable, Hashable {
case placed
case preparing
case enRoute
case delivered
/// Human-readable label for display.
var label: String {
switch self {
case .placed: return "Order Placed"
case .preparing: return "Preparing"
case .enRoute: return "On the Way"
case .delivered: return "Delivered"
}
}
/// SF Symbol name for each status.
var systemImage: String {
switch self {
case .placed: return "checkmark.circle"
case .preparing: return "frying.pan"
case .enRoute: return "car.fill"
case .delivered: return "bag.fill"
}
}
}Live Activity Widget View
This file lives in the widget extension target. It defines all presentation surfaces: Lock Screen, Dynamic Island compact, Dynamic Island expanded, and minimal.
import ActivityKit
import SwiftUI
import WidgetKit
/// The Live Activity configuration that provides all presentation layouts.
struct DeliveryLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: DeliveryActivityAttributes.self) { context in
// MARK: - Lock Screen / StandBy Presentation
LockScreenView(context: context)
} dynamicIsland: { context in
DynamicIsland {
// MARK: - Expanded Regions
DynamicIslandExpandedRegion(.leading) {
ExpandedLeadingView(context: context)
}
DynamicIslandExpandedRegion(.trailing) {
ExpandedTrailingView(context: context)
}
DynamicIslandExpandedRegion(.center) {
ExpandedCenterView(context: context)
}
DynamicIslandExpandedRegion(.bottom) {
ExpandedBottomView(context: context)
}
} compactLeading: {
// MARK: - Compact Leading
CompactLeadingView(context: context)
} compactTrailing: {
// MARK: - Compact Trailing
CompactTrailingView(context: context)
} minimal: {
// MARK: - Minimal
MinimalView(context: context)
}
}
}
}
// MARK: - Lock Screen View
/// The primary Lock Screen and StandBy presentation.
private struct LockScreenView: View {
let context: ActivityViewContext<DeliveryActivityAttributes>
var body: some View {
HStack(spacing: 12) {
// Status icon
Image(systemName: context.state.status.systemImage)
.font(.title2)
.foregroundStyle(.white)
.frame(width: 44, height: 44)
.background(.blue.gradient, in: Circle())
VStack(alignment: .leading, spacing: 2) {
Text(context.state.status.label)
.font(.headline)
Text(context.attributes.restaurantName)
.font(.subheadline)
.foregroundStyle(.secondary)
}
Spacer()
VStack(alignment: .trailing, spacing: 2) {
Text(context.state.estimatedDelivery, style: .timer)
.font(.title3.monospacedDigit())
.foregroundStyle(.blue)
if let driver = context.state.driverName {
Text(driver)
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
.padding()
.activityBackgroundTint(.black.opacity(0.8))
.activitySystemActionForegroundColor(.white)
}
}
// MARK: - Dynamic Island Expanded Views
/// Leading region of the expanded Dynamic Island.
private struct ExpandedLeadingView: View {
let context: ActivityViewContext<DeliveryActivityAttributes>
var body: some View {
Image(systemName: context.state.status.systemImage)
.font(.title2)
.foregroundStyle(.blue)
}
}
/// Trailing region of the expanded Dynamic Island.
private struct ExpandedTrailingView: View {
let context: ActivityViewContext<DeliveryActivityAttributes>
var body: some View {
Text(context.state.estimatedDelivery, style: .timer)
.font(.callout.monospacedDigit())
.foregroundStyle(.blue)
.multilineTextAlignment(.trailing)
}
}
/// Center region of the expanded Dynamic Island.
private struct ExpandedCenterView: View {
let context: ActivityViewContext<DeliveryActivityAttributes>
var body: some View {
VStack(spacing: 2) {
Text(context.state.status.label)
.font(.headline)
Text("Order #\(context.attributes.orderNumber)")
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
/// Bottom region of the expanded Dynamic Island.
private struct ExpandedBottomView: View {
let context: ActivityViewContext<DeliveryActivityAttributes>
var body: some View {
HStack {
Text(context.attributes.restaurantName)
.font(.caption)
.foregroundStyle(.secondary)
Spacer()
if let driver = context.state.driverName {
Label(driver, systemImage: "person.fill")
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
}
// MARK: - Dynamic Island Compact Views
/// Compact leading view -- shown in the left pill of the Dynamic Island.
private struct CompactLeadingView: View {
let context: ActivityViewContext<DeliveryActivityAttributes>
var body: some View {
Image(systemName: context.state.status.systemImage)
.foregroundStyle(.blue)
}
}
/// Compact trailing view -- shown in the right pill of the Dynamic Island.
private struct CompactTrailingView: View {
let context: ActivityViewContext<DeliveryActivityAttributes>
var body: some View {
Text(context.state.estimatedDelivery, style: .timer)
.font(.caption2.monospacedDigit())
.foregroundStyle(.blue)
}
}
// MARK: - Minimal View
/// Minimal presentation -- shown when multiple Live Activities are active.
private struct MinimalView: View {
let context: ActivityViewContext<DeliveryActivityAttributes>
var body: some View {
Image(systemName: context.state.status.systemImage)
.foregroundStyle(.blue)
}
}
// MARK: - Previews
#if DEBUG
struct DeliveryLiveActivity_Previews: PreviewProvider {
static let attributes = DeliveryActivityAttributes(
orderNumber: "1234",
restaurantName: "Pizza Place"
)
static let contentState = DeliveryActivityAttributes.ContentState(
status: .enRoute,
estimatedDelivery: Date().addingTimeInterval(15 * 60),
driverName: "Alex"
)
static var previews: some View {
attributes
.previewContext(contentState, viewKind: .content)
.previewDisplayName("Lock Screen")
attributes
.previewContext(contentState, viewKind: .dynamicIsland(.compact))
.previewDisplayName("Compact")
attributes
.previewContext(contentState, viewKind: .dynamicIsland(.expanded))
.previewDisplayName("Expanded")
attributes
.previewContext(contentState, viewKind: .dynamicIsland(.minimal))
.previewDisplayName("Minimal")
}
}
#endifActivity Lifecycle Manager
This file lives in the main app target. It provides a protocol-based interface for starting, updating, and ending Live Activities.
Protocol
import ActivityKit
import Foundation
/// Protocol defining Live Activity lifecycle operations.
///
/// Use this protocol to abstract activity management for testing and
/// to swap implementations (e.g., mock manager in previews).
protocol LiveActivityManaging: Sendable {
associatedtype Attributes: ActivityAttributes
/// Start a new Live Activity.
/// - Parameters:
/// - attributes: Static attributes for the activity.
/// - state: Initial content state.
/// - staleDate: Optional date after which the system considers the content stale.
/// - Returns: The activity ID string.
@discardableResult
func startActivity(
attributes: Attributes,
state: Attributes.ContentState,
staleDate: Date?
) async throws -> String
/// Update the currently running Live Activity.
/// - Parameters:
/// - state: New content state.
/// - alertConfiguration: Optional alert to display with the update.
/// - staleDate: Optional new stale date.
func updateActivity(
state: Attributes.ContentState,
alertConfiguration: AlertConfiguration?,
staleDate: Date?
) async
/// End the currently running Live Activity.
/// - Parameters:
/// - state: Final content state to display.
/// - dismissalPolicy: When the system should remove the activity from the Lock Screen.
/// - alertConfiguration: Optional alert to display when ending.
func endActivity(
state: Attributes.ContentState?,
dismissalPolicy: ActivityUIDismissalPolicy,
alertConfiguration: AlertConfiguration?
) async
/// End all running Live Activities for this attribute type.
func endAllActivities() async
/// The push token for the current activity, if available.
var currentPushToken: Data? { get }
/// Whether a Live Activity is currently running.
var isActivityActive: Bool { get }
}
// MARK: - Default Parameter Values
extension LiveActivityManaging {
@discardableResult
func startActivity(
attributes: Attributes,
state: Attributes.ContentState,
staleDate: Date? = nil
) async throws -> String {
try await startActivity(attributes: attributes, state: state, staleDate: staleDate)
}
func updateActivity(
state: Attributes.ContentState,
alertConfiguration: AlertConfiguration? = nil,
staleDate: Date? = nil
) async {
await updateActivity(state: state, alertConfiguration: alertConfiguration, staleDate: staleDate)
}
func endActivity(
state: Attributes.ContentState? = nil,
dismissalPolicy: ActivityUIDismissalPolicy = .default,
alertConfiguration: AlertConfiguration? = nil
) async {
await endActivity(state: state, dismissalPolicy: dismissalPolicy, alertConfiguration: alertConfiguration)
}
}Concrete Implementation
import ActivityKit
import Foundation
import os.log
/// Manages the lifecycle of a Live Activity for the given attributes type.
///
/// Usage:
/// ```swift
/// let manager = LiveActivityManager<DeliveryActivityAttributes>()
/// try await manager.startActivity(attributes: attributes, state: initialState)
/// ```
@MainActor
final class LiveActivityManager<Attributes: ActivityAttributes>: LiveActivityManaging {
// MARK: - Properties
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "LiveActivity")
/// The currently running activity, if any.
private(set) var currentActivity: Activity<Attributes>?
/// Task that observes push token updates.
private var pushTokenTask: Task<Void, Never>?
/// Callback invoked when the push token changes. Send this token to your server.
var onPushTokenUpdate: ((Data) -> Void)?
/// The current push token for the running activity.
var currentPushToken: Data? {
currentActivity?.pushToken
}
/// Whether a Live Activity is currently active.
var isActivityActive: Bool {
guard let currentActivity else { return false }
return currentActivity.activityState == .active
}
// MARK: - Initialization
init() {}
deinit {
pushTokenTask?.cancel()
}
// MARK: - Start
/// Start a new Live Activity.
@discardableResult
func startActivity(
attributes: Attributes,
state: Attributes.ContentState,
staleDate: Date? = nil
) async throws -> String {
// Check if Live Activities are enabled
guard ActivityAuthorizationInfo().areActivitiesEnabled else {
logger.error("Live Activities are not enabled on this device.")
throw LiveActivityError.activitiesNotEnabled
}
// End any existing activity before starting a new one
if currentActivity != nil {
await endActivity()
}
let content = ActivityContent(
state: state,
staleDate: staleDate
)
let activity = try Activity.request(
attributes: attributes,
content: content,
pushType: .token // Remove this parameter if not using push updates
)
currentActivity = activity
logger.info("Started Live Activity: \(activity.id)")
// Begin observing push token updates
observePushTokenUpdates(for: activity)
return activity.id
}
// MARK: - Update
/// Update the running Live Activity with new state.
func updateActivity(
state: Attributes.ContentState,
alertConfiguration: AlertConfiguration? = nil,
staleDate: Date? = nil
) async {
guard let currentActivity else {
logger.warning("No active Live Activity to update.")
return
}
let content = ActivityContent(
state: state,
staleDate: staleDate
)
await currentActivity.update(
content,
alertConfiguration: alertConfiguration
)
logger.info("Updated Live Activity: \(currentActivity.id)")
}
// MARK: - End
/// End the running Live Activity.
func endActivity(
state: Attributes.ContentState? = nil,
dismissalPolicy: ActivityUIDismissalPolicy = .default,
alertConfiguration: AlertConfiguration? = nil
) async {
guard let currentActivity else {
logger.warning("No active Live Activity to end.")
return
}
let finalContent: ActivityContent<Attributes.ContentState>?
if let state {
finalContent = ActivityContent(state: state, staleDate: nil)
} else {
finalContent = nil
}
await currentActivity.end(
finalContent,
dismissalPolicy: dismissalPolicy
)
logger.info("Ended Live Activity: \(currentActivity.id)")
pushTokenTask?.cancel()
pushTokenTask = nil
self.currentActivity = nil
}
/// End all running Live Activities for this attribute type.
func endAllActivities() async {
for activity in Activity<Attributes>.activities {
await activity.end(nil, dismissalPolicy: .immediate)
logger.info("Ended activity: \(activity.id)")
}
pushTokenTask?.cancel()
pushTokenTask = nil
currentActivity = nil
}
// MARK: - Push Token Observation
/// Observe push token updates for the given activity.
/// The push token can change during the activity lifetime; always send the latest to your server.
private func observePushTokenUpdates(for activity: Activity<Attributes>) {
pushTokenTask?.cancel()
pushTokenTask = Task {
for await token in activity.pushTokenUpdates {
let tokenString = token.map { String(format: "%02x", $0) }.joined()
logger.info("Push token updated: \(tokenString)")
onPushTokenUpdate?(token)
}
}
}
// MARK: - Query
/// Returns all currently running activities for this attribute type.
func allActivities() -> [Activity<Attributes>] {
Activity<Attributes>.activities
}
/// Check if Live Activities are enabled on this device.
var areActivitiesEnabled: Bool {
ActivityAuthorizationInfo().areActivitiesEnabled
}
}
// MARK: - Errors
/// Errors that can occur during Live Activity lifecycle management.
enum LiveActivityError: LocalizedError {
case activitiesNotEnabled
case activityNotFound
case startFailed(underlying: Error)
var errorDescription: String? {
switch self {
case .activitiesNotEnabled:
return "Live Activities are not enabled. The user may have disabled them in Settings."
case .activityNotFound:
return "No active Live Activity was found."
case .startFailed(let error):
return "Failed to start Live Activity: \(error.localizedDescription)"
}
}
}Alert Configuration Helper
import ActivityKit
/// Convenience factory for creating alert configurations.
///
/// Alerts are displayed on the Lock Screen when an activity is updated or ended.
/// They are only shown when the device is locked.
enum LiveActivityAlertBuilder {
/// Create an alert configuration for an activity update.
/// - Parameters:
/// - title: The alert title shown on the Lock Screen.
/// - body: The alert body text.
/// - sound: The alert sound. Defaults to `.default`.
/// - Returns: An `AlertConfiguration` instance.
static func updateAlert(
title: String,
body: String,
sound: AlertConfiguration.AlertSound = .default
) -> AlertConfiguration {
AlertConfiguration(
title: LocalizedStringResource(stringLiteral: title),
body: LocalizedStringResource(stringLiteral: body),
sound: sound
)
}
/// Create an alert configuration for ending an activity.
static func endAlert(
title: String,
body: String,
sound: AlertConfiguration.AlertSound = .default
) -> AlertConfiguration {
AlertConfiguration(
title: LocalizedStringResource(stringLiteral: title),
body: LocalizedStringResource(stringLiteral: body),
sound: sound
)
}
}Authorization Observation
Observe whether the user has enabled or disabled Live Activities in Settings.
import ActivityKit
import Foundation
/// Observes Live Activity authorization changes.
///
/// Use this to update your UI when the user toggles Live Activities in Settings.
@MainActor
@Observable
final class LiveActivityAuthObserver {
/// Whether Live Activities are currently enabled.
private(set) var areActivitiesEnabled: Bool
private var observationTask: Task<Void, Never>?
init() {
let authInfo = ActivityAuthorizationInfo()
areActivitiesEnabled = authInfo.areActivitiesEnabled
observationTask = Task {
for await enabled in authInfo.activityEnablementUpdates {
self.areActivitiesEnabled = enabled
}
}
}
deinit {
observationTask?.cancel()
}
}SwiftUI Integration View
A reusable SwiftUI view that shows activity status and provides controls.
import SwiftUI
import ActivityKit
/// A view that displays the current Live Activity state and provides controls.
///
/// Usage:
/// ```swift
/// LiveActivityControlView(manager: deliveryActivityManager)
/// ```
struct LiveActivityControlView<Attributes: ActivityAttributes>: View {
let manager: LiveActivityManager<Attributes>
@State private var authObserver = LiveActivityAuthObserver()
var body: some View {
Group {
if !authObserver.areActivitiesEnabled {
ContentUnavailableView(
"Live Activities Disabled",
systemImage: "bell.slash",
description: Text("Enable Live Activities in Settings to see real-time updates on your Lock Screen and Dynamic Island.")
)
} else if manager.isActivityActive {
activeActivityView
} else {
noActivityView
}
}
}
private var activeActivityView: some View {
VStack(spacing: 16) {
Label("Live Activity Running", systemImage: "dot.radiowaves.left.and.right")
.font(.headline)
.foregroundStyle(.green)
if let activityId = manager.currentActivity?.id {
Text("ID: \(activityId)")
.font(.caption)
.foregroundStyle(.secondary)
}
Button("End Activity", role: .destructive) {
Task {
await manager.endActivity(dismissalPolicy: .immediate)
}
}
.buttonStyle(.bordered)
}
.padding()
}
private var noActivityView: some View {
VStack(spacing: 12) {
Image(systemName: "bell.badge")
.font(.largeTitle)
.foregroundStyle(.secondary)
Text("No Active Live Activity")
.font(.headline)
.foregroundStyle(.secondary)
}
.padding()
}
}Widget Bundle Integration
If the project already has a widget bundle, add the Live Activity to it.
import SwiftUI
import WidgetKit
@main
struct MyAppWidgets: WidgetBundle {
var body: some Widget {
// Existing widgets
MyHomeScreenWidget()
// Live Activity
DeliveryLiveActivity()
}
}If the widget extension only contains the Live Activity, the @main attribute can go directly on the Widget struct:
@main
struct DeliveryLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: DeliveryActivityAttributes.self) { context in
// Lock Screen view...
} dynamicIsland: { context in
// Dynamic Island views...
}
}
}Push-to-Update Server Payload Reference
Update Payload
{
"aps": {
"timestamp": 1234567890,
"event": "update",
"content-state": {
"status": "enRoute",
"estimatedDelivery": 1234568000,
"driverName": "Alex"
},
"stale-date": 1234569000,
"alert": {
"title": "Order Update",
"body": "Your driver Alex is on the way!"
}
}
}End Payload
{
"aps": {
"timestamp": 1234567890,
"event": "end",
"dismissal-date": 1234571490,
"content-state": {
"status": "delivered",
"estimatedDelivery": 1234567890,
"driverName": "Alex"
},
"alert": {
"title": "Delivered!",
"body": "Your order from Pizza Place has arrived."
}
}
}Key Fields
| Field | Required | Description |
|---|---|---|
timestamp | Yes | Unix timestamp (seconds). Must be greater than previous update. |
event | Yes | "update" or "end". |
content-state | Yes | Must match the ContentState Codable structure exactly. |
stale-date | No | Unix timestamp after which the system considers content stale. |
dismissal-date | No | Unix timestamp when the ended activity should be removed from Lock Screen. |
alert | No | Lock Screen alert shown with the update. Only shown when device is locked. |
APNs Headers
| Header | Value |
|---|---|
apns-topic | {bundle-id}.push-type.liveactivity |
apns-push-type | liveactivity |
apns-priority | 5 (low) or 10 (high, for time-sensitive updates) |
Patterns: Good and Bad
Starting Activities
// ✅ Good: Check authorization before starting
guard ActivityAuthorizationInfo().areActivitiesEnabled else {
showEnableActivitiesPrompt()
return
}
let activity = try Activity.request(attributes: attrs, content: content, pushType: .token)
// ❌ Bad: Start without checking authorization
let activity = try Activity.request(attributes: attrs, content: content, pushType: .token)Push Token Handling
// ✅ Good: Continuously observe token changes
for await token in activity.pushTokenUpdates {
await sendTokenToServer(token)
}
// ❌ Bad: Read token once and assume it stays the same
let token = activity.pushToken
sendTokenToServer(token)Ending Activities
// ✅ Good: Provide final state when ending
await activity.end(
ActivityContent(state: finalState, staleDate: nil),
dismissalPolicy: .after(Date().addingTimeInterval(4 * 3600))
)
// ❌ Bad: End with nil content, leaving stale data displayed
await activity.end(nil, dismissalPolicy: .default)Content State Size
// ✅ Good: Keep ContentState small and Codable
struct ContentState: Codable, Hashable {
var score: Int
var timeRemaining: TimeInterval
var statusMessage: String
}
// ❌ Bad: Large or complex data in ContentState
struct ContentState: Codable, Hashable {
var allPlayerStats: [PlayerStats] // Array can easily exceed 4KB
var fullGameLog: String // Unbounded string
var imageData: Data // Binary data
}Stale Dates
// ✅ Good: Set a stale date so the system can indicate outdated content
let content = ActivityContent(
state: state,
staleDate: Date().addingTimeInterval(15 * 60)
)
// ❌ Bad: No stale date; user sees outdated data with no indication
let content = ActivityContent(state: state, staleDate: nil)