Loading...
Loading...
Implement, review, or improve Live Activities and Dynamic Island experiences in iOS apps using ActivityKit. Use when building real-time updating widgets for the Lock Screen and Dynamic Island — delivery tracking, sports scores, ride-sharing status, workout timers, media playback, or any time-sensitive information that updates in real time. Trigger for any task involving ActivityKit, ActivityAttributes, Activity lifecycle (request/update/end), Dynamic Island layouts (compact/minimal/expanded), push-to-update Live Activities, or Lock Screen live widgets.
npx skill4agent add dpearson2699/swift-ios-skills live-activitiesreferences/live-activity-patterns.mdNSSupportsLiveActivities = YESActivityAttributesContentStateActivityConfigurationActivity.request(attributes:content:pushType:)activity.update(_:)activity.end(_:dismissalPolicy:)ContentStateContentStateimport ActivityKit
struct DeliveryAttributes: ActivityAttributes {
// Static -- set once at activity creation, never changes
var orderNumber: Int
var restaurantName: String
// Dynamic -- updated throughout the activity lifetime
struct ContentState: Codable, Hashable {
var driverName: String
var estimatedDeliveryTime: ClosedRange<Date>
var currentStep: DeliveryStep
}
}
enum DeliveryStep: String, Codable, Hashable, CaseIterable {
case confirmed, preparing, pickedUp, delivering, delivered
var icon: String {
switch self {
case .confirmed: "checkmark.circle"
case .preparing: "frying.pan"
case .pickedUp: "bag.fill"
case .delivering: "box.truck.fill"
case .delivered: "house.fill"
}
}
}staleDateActivityContentcontext.isStaletruelet content = ActivityContent(
state: state,
staleDate: Date().addingTimeInterval(300), // stale after 5 minutes
relevanceScore: 75
)Activity.request.tokenpushTypelet attributes = DeliveryAttributes(orderNumber: 42, restaurantName: "Pizza Place")
let state = DeliveryAttributes.ContentState(
driverName: "Alex",
estimatedDeliveryTime: Date()...Date().addingTimeInterval(1800),
currentStep: .preparing
)
let content = ActivityContent(state: state, staleDate: nil, relevanceScore: 75)
do {
let activity = try Activity.request(
attributes: attributes,
content: content,
pushType: .token
)
print("Started activity: \(activity.id)")
} catch {
print("Failed to start activity: \(error)")
}AlertConfigurationlet updatedState = DeliveryAttributes.ContentState(
driverName: "Alex",
estimatedDeliveryTime: Date()...Date().addingTimeInterval(600),
currentStep: .delivering
)
let updatedContent = ActivityContent(
state: updatedState,
staleDate: Date().addingTimeInterval(300),
relevanceScore: 90
)
// Silent update
await activity.update(updatedContent)
// Update with an alert
await activity.update(updatedContent, alertConfiguration: AlertConfiguration(
title: "Order Update",
body: "Your driver is nearby!",
sound: .default
))let finalState = DeliveryAttributes.ContentState(
driverName: "Alex",
estimatedDeliveryTime: Date()...Date(),
currentStep: .delivered
)
let finalContent = ActivityContent(state: finalState, staleDate: nil, relevanceScore: 0)
// System decides when to remove (up to 4 hours)
await activity.end(finalContent, dismissalPolicy: .default)
// Remove immediately
await activity.end(finalContent, dismissalPolicy: .immediate)
// Remove after a specific time (max 4 hours from now)
await activity.end(finalContent, dismissalPolicy: .after(Date().addingTimeInterval(3600)))struct DeliveryActivityWidget: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: DeliveryAttributes.self) { context in
// Lock Screen / StandBy / CarPlay / Mac menu bar content
VStack(alignment: .leading, spacing: 8) {
HStack {
Text(context.attributes.restaurantName)
.font(.headline)
Spacer()
Text("Order #\(context.attributes.orderNumber)")
.font(.caption)
.foregroundStyle(.secondary)
}
if context.isStale {
Label("Updating...", systemImage: "arrow.trianglehead.2.clockwise")
.font(.subheadline)
.foregroundStyle(.secondary)
} else {
HStack {
Label(context.state.driverName, systemImage: "person.fill")
Spacer()
Text(timerInterval: context.state.estimatedDeliveryTime,
countsDown: true)
.monospacedDigit()
}
.font(.subheadline)
// Progress steps
HStack(spacing: 12) {
ForEach(DeliveryStep.allCases, id: \.self) { step in
Image(systemName: step.icon)
.foregroundStyle(
step <= context.state.currentStep ? .primary : .tertiary
)
}
}
}
}
.padding()
} dynamicIsland: { context in
// Dynamic Island closures (see next section)
DynamicIsland {
// Expanded regions...
DynamicIslandExpandedRegion(.leading) {
Image(systemName: "box.truck.fill").font(.title2)
}
DynamicIslandExpandedRegion(.trailing) {
Text(timerInterval: context.state.estimatedDeliveryTime,
countsDown: true)
.font(.caption).monospacedDigit()
}
DynamicIslandExpandedRegion(.center) {
Text(context.attributes.restaurantName).font(.headline)
}
DynamicIslandExpandedRegion(.bottom) {
HStack(spacing: 12) {
ForEach(DeliveryStep.allCases, id: \.self) { step in
Image(systemName: step.icon)
.foregroundStyle(
step <= context.state.currentStep ? .primary : .tertiary
)
}
}
}
} compactLeading: {
Image(systemName: "box.truck.fill")
} compactTrailing: {
Text(timerInterval: context.state.estimatedDeliveryTime,
countsDown: true)
.frame(width: 40).monospacedDigit()
} minimal: {
Image(systemName: "box.truck.fill")
}
}
}
}supplementalActivityFamilies.small.mediumActivityConfiguration(for: DeliveryAttributes.self) { context in
// Lock Screen content
} dynamicIsland: { context in
// Dynamic Island
}
.supplementalActivityFamilies([.small, .medium])| Region | Purpose |
|---|---|
| Icon or tiny label identifying the activity |
| One key value (timer, score, status) |
| Region | Position |
|---|---|
| Left of the TrueDepth camera; wraps below |
| Right of the TrueDepth camera; wraps below |
| Directly below the camera |
| Below all other regions |
DynamicIsland { /* expanded */ }
compactLeading: { /* ... */ }
compactTrailing: { /* ... */ }
minimal: { /* ... */ }
.keylineTint(.blue).tokenpushTypelet activity = try Activity.request(
attributes: attributes,
content: content,
pushType: .token
)
// Observe token changes -- tokens can rotate
Task {
for await token in activity.pushTokenUpdates {
let tokenString = token.map { String(format: "%02x", $0) }.joined()
try await ServerAPI.shared.registerActivityToken(
tokenString, activityID: activity.id
)
}
}apns-push-type: liveactivityapns-topic: <bundle-id>.push-type.liveactivityapns-priority: 510{
"aps": {
"timestamp": 1700000000,
"event": "update",
"content-state": {
"driverName": "Alex",
"estimatedDeliveryTime": {
"lowerBound": 1700000000,
"upperBound": 1700001800
},
"currentStep": "delivering"
},
"stale-date": 1700000300,
"alert": {
"title": "Delivery Update",
"body": "Your driver is nearby!"
}
}
}"event": "end""dismissal-date"content-stateContentStateTask {
for await token in Activity<DeliveryAttributes>.pushToStartTokenUpdates {
let tokenString = token.map { String(format: "%02x", $0) }.joined()
try await ServerAPI.shared.registerPushToStartToken(tokenString)
}
}NSSupportsLiveActivitiesFrequentUpdates = YESlet scheduledDate = Calendar.current.date(
from: DateComponents(year: 2026, month: 3, day: 15, hour: 19, minute: 0)
)!
let activity = try Activity.request(
attributes: attributes,
content: content,
pushType: .token,
start: scheduledDate
).standard.transient.transientlet activity = try Activity.request(
attributes: attributes, content: content,
pushType: .token, style: .transient
).channellet activity = try Activity.request(
attributes: attributes, content: content,
pushType: .channel("delivery-updates")
)context.isStaleactivity.pushTokenUpdatesNSSupportsLiveActivitiesNSSupportsLiveActivities = YEScontentStateActivityContentContentStateActivityAttributesContentStateNSSupportsLiveActivities = YESActivityContentcontext.isStaleactivity.pushTokenUpdatesAlertConfigurationActivityAuthorizationInforeferences/live-activity-patterns.md