Mobile Development 17 min read

Mastering iOS Live Activities: Setup, Real‑Time Updates, and Push Integration

This comprehensive guide walks you through Apple’s Live Activity feature introduced at WWDC22, covering scenario planning, project configuration for mixed ObjC‑Swift or pure Swift code, widget implementation with ActivityKit, creating, updating, ending activities, observing state and push tokens, server‑side push payloads, and handling technical constraints such as network image loading.

Sohu Smart Platform Tech Team
Sohu Smart Platform Tech Team
Sohu Smart Platform Tech Team
Mastering iOS Live Activities: Setup, Real‑Time Updates, and Push Integration

Background

At WWDC22 Apple introduced the Live Activity concept, allowing apps to show real‑time updates on the lock screen. ActivityKit provides the API to create custom Dynamic Island views. The following article reveals the implementation details and practical tips.

Scenario and Configuration

Live Activity is available on iOS 16.1+ lock‑screen and on iPhone 14 Pro+ devices with a Dynamic Island. Compared with iOS 16.0 lock‑screen widgets, Live Activities appear in the notification area, support more flexible view customization and refresh mechanisms, and are suitable for use cases such as fitness tracking, order status, sports scores, and breaking news.

Scenario Limitations

Maximum duration is 8 hours; after 12 hours the activity disappears automatically.

The activity must be created while the app is in the foreground; it cannot appear when the app is not running.

Only 4 KB of data can be sent via push; larger payloads result in a non‑200 APNs response.

Multiple cards with similar styles collapse, so creating many cards simultaneously is discouraged.

Push‑based activities should keep data size small because push updates are highly automated and limited by the same 4 KB constraint.

Project Configuration

1. Mixed ObjC & Swift

If your project mixes ObjC and Swift, you need to add a Swift file to the main target for bridging because ActivityKit is Swift‑only and requires SwiftUI for the UI.

Create a Swift file in the main target to host the ActivityKit code.

Add a new target that depends on the main target and the ActivityKit target.

The ActivityKit target contains the live‑activity UI and data handling.

2. Swift Only

For pure Swift projects, no bridging file is needed; the steps are the same as above.

Both project types must add NSSupportsLiveActivities with value YES to the Info.plist of the main app.

Overall Process Implementation

Initial Code

The file resides in the ActivityKit target and manages the lifecycle of the live activity.

struct LiveActivitiesWidget: Widget {
    var body: some WidgetConfiguration {
        ActivityConfiguration(for: LiveActivitiesAttributes.self) { context in
            Text("锁屏上的界面")
                .activityBackgroundTint(.cyan)
                .activitySystemActionForegroundColor(.black)
        } dynamicIsland: { context in
            DynamicIsland {
                DynamicIslandExpandedRegion(.leading) { Text("灵动岛展开后的左边") }
                DynamicIslandExpandedRegion(.trailing) { Text("灵动岛展开后的右边") }
                DynamicIslandExpandedRegion(.center) { Text("灵动岛展开后的中心") }
                DynamicIslandExpandedRegion(.bottom) { Text("灵动岛展开后的底部") }
            } compactLeading: { Text("灵动岛未展开的左边") }
              compactTrailing: { Text("灵动岛未展开的右边") }
              minimal: { Text("灵动岛Mini") }
            .widgetURL(URL(string: "http://www.apple.com"))
            .keylineTint(.red)
        }
    }
}

Define Data Model

The model must inherit from ActivityAttributes provided by ActivityKit.

struct ActivityWidgetAttributes: ActivityAttributes {
    public struct ContentState: Codable, Hashable {
        var process: Double
        var title: String
        // ... other dynamic fields
    }
    var icon: String // static field
}

Dynamic updates modify the ContentState part, while static fields remain unchanged.

Main App Part

1. Create

public static func request(attributes: Attributes, contentState: Activity<Attributes>.ContentState, pushType: PushType? = nil) throws -> Activity<Attributes>
private var myActivity: Activity<ActivityWidgetAttributes>?
let initialContentState = ActivityWidgetAttributes.ContentState(process: 0.6, title: "this is a title")
let activityAttributes = ActivityWidgetAttributes(icon: "XiaoBu")
myActivity = try Activity.request(attributes: activityAttributes, contentState: initialContentState, pushType: .token)
Task {
    for await data in activity.pushTokenUpdates {
        let myToken = data.map { String(format: "%02x", $0) }.joined()
        let activityId = activity.id
        print("Activity id : \(activityId) and token \(myToken)")
        observeActivity(activity: activity)
    }
}

Push‑based creation is asynchronous; you must wait for the token before sending it to your backend.

From iOS 17.2 onward you can obtain a push‑to‑start token without launching the activity, using pushToStartTokenUpdates.

for try await pushToken in Activity<ActivityWidgetAttributes>.pushToStartTokenUpdates {
    // send token to backend
}

2. Update

public func update(using contentState: Activity<Attributes>.ContentState, alertConfiguration: AlertConfiguration? = nil) async
let updateStatus = ActivityWidgetAttributes.ContentState(nickName: "Augus123")
let alertConfiguration = AlertConfiguration(title: "111", body: "2222", sound: .default)
Task {
    await myActivity?.update(using: updateStatus, alertConfiguration: alertConfiguration)
}

iOS 17.2 adds an alertConfiguration parameter for richer notifications.

3. End

public func end(using contentState: Activity<Attributes>.ContentState? = nil, dismissalPolicy: ActivityUIDismissalPolicy = .default) async
await myActivity?.end(using: nil, dismissalPolicy: .immediate)

The default policy keeps the activity on the lock screen for up to four hours after ending; .immediate removes it instantly.

4. State Retrieval and Push Update Callbacks

After creation you can observe state changes via activityStateUpdates and content updates via contentUpdates. Push token updates are also observable.

func observeActivity(activity: Activity<SNActivityLiveTextImagetAttributes>) {
    Task {
        await withTaskGroup(of: Void.self) { group in
            group.addTask {
                for await activityState in activity.activityStateUpdates {
                    if activityState == .dismissed { /* handle dismissal */ }
                    else if activityState == .ended { /* handle end */ }
                }
            }
            group.addTask {
                for await contentState in activity.contentUpdates {
                    // download image, then update UI
                    await activity.update(contentState)
                }
            }
            group.addTask {
                for await pushToken in activity.pushTokenUpdates {
                    let tokenString = pushToken.hexadecimalString
                    // send token to backend
                }
            }
        }
    }
}

In practice the callback success rate is about 93.33 %.

5. Permissions

Live Activity permission cannot be observed directly; you must check ActivityAuthorizationInfo().areActivitiesEnabled before creating the activity.

ActivityAuthorizationInfo().areActivitiesEnabled

Server Side

// Push configuration (environment variables)
TEAM_ID=your_team_id
AUTH_KEY_ID=your_key_id
TOPIC=${BundleIdentifier}.push-type.liveactivity
DEVICE_TOKEN=your_device_token
APNS_HOST_NAME=api.sandbox.push.apple.com   // or api.push.apple.com for production

// APNs payload example
{
  "aps": {
    "timestamp": 1666667682,
    "event": "update",
    "content-state": { "process": 0.7, "title": "更新的title" },
    "alert": { "title": "Track Update", "body": "Tony Stark is now handling the delivery!" }
  }
}

Technical Challenges and Strategies

Network Image Display

Technical Limitations

Live Activity extensions cannot perform network requests (e.g., AsyncImage or URLSession). To show remote images you must download them in the main app beforehand and store them in an App Group shared container.

Solution

private func downloadImage(from url: URL) async throws -> URL? {
    guard var destination = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.test.liveactivityappgroup") else { return nil }
    destination = destination.appendingPathComponent(url.lastPathComponent)
    guard !FileManager.default.fileExists(atPath: destination.path()) else { return destination }
    let (source, _) = try await URLSession.shared.download(from: url)
    try FileManager.default.moveItem(at: source, to: destination)
    return destination
}

In the ActivityKit target, read the image from the shared container:

if let imageContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.test.liveactivityappgroup")?.appendingPathComponent(context.state.imageName),
   let uiImage = UIImage(contentsOfFile: imageContainer.path()) {
    Image(uiImage: uiImage)
        .resizable()
        .aspectRatio(contentMode: .fill)
        .frame(width: 50, height: 50)
}

References

Live Activities UI

https://developer.apple.com/design/human-interface-guidelines/live-activities

Live Activities API

https://developer.apple.com/documentation/activitykit/displaying-live-data-with-live-activities

Live Activity Push Notifications

https://developer.apple.com/documentation/activitykit/starting-and-updating-live-activities-with-activitykit-push-notifications

How to fetch an image in live activity

https://forums.developer.apple.com/forums/thread/716902
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

iOSpush notificationsSwiftActivityKitDynamic IslandLive ActivitiesApp Group
Sohu Smart Platform Tech Team
Written by

Sohu Smart Platform Tech Team

The Sohu News app's technical sharing hub, offering deep tech analyses, the latest industry news, and fun developer anecdotes. Follow us to discover the team's daily joys.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.