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.
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().areActivitiesEnabledServer 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-activitiesLive Activities API
https://developer.apple.com/documentation/activitykit/displaying-live-data-with-live-activitiesLive Activity Push Notifications
https://developer.apple.com/documentation/activitykit/starting-and-updating-live-activities-with-activitykit-push-notificationsHow to fetch an image in live activity
https://forums.developer.apple.com/forums/thread/716902Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
