关于 live-activities
live-activities 是用于构建和审查 ActivityKit 体验的 Swift 开发技能,面向开发实时速览界面的开发者。内容涵盖活动生命周期、Dynamic Island 布局、推送更新,以及较新的定时和显示选项。
主要功能
- 01ActivityKit 设置与生命周期模式
- 02锁定屏幕和 Dynamic Island 布局
- 03APNs 推送更新与远程启动
- 04定时活动与基于频道的活动
- 05代码审查清单与常见问题
适用场景
- 01在锁定屏幕显示配送进度
- 02发送由服务器驱动的活动更新
- 03为未来事件安排活动
- 04审查 Live Activity 实现
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dpearson2699/ swift-ios-skills --skill live-activities安装后在 Claude Code 中输入/live-activities确认已加载。
附带文件references/live-activity-patterns.md
详细介绍
实时活动与灵动岛
使用 ActivityKit,在锁定屏幕、灵动岛、待机模式、CarPlay 和 Mac 菜单栏中构建实时、一目了然的体验。设计模式面向 iOS 26+ 和 Swift 6.2,除非另有说明,否则向后兼容至 iOS 16.1。
有关完整代码模式(包括推送负载格式、并发活动、状态观察和测试),请参阅 references/live-activity-patterns.md。
目录
工作流
1. 创建新的实时活动
- 将
NSSupportsLiveActivities = YES添加到宿主应用的 Info.plist。 - 定义一个带有嵌套
ContentState的ActivityAttributes结构体。 - 在 Widget 包中创建
ActivityConfiguration,并提供锁定屏幕内容和灵动岛闭包。 - 使用
Activity.request(attributes:content:pushType:)启动活动。 - 使用
activity.update(_:)更新活动,并使用activity.end(_:dismissalPolicy:)结束活动。 - 将推送令牌转发到你的服务端,以支持远程更新。
2. 审查现有实时活动代码
按照本文档末尾的审核清单逐项检查。
ActivityAttributes 定义
定义静态数据(在活动生命周期内不可变)和动态 ContentState(随每次更新而变化)。请保持 ContentState 简洁,因为每次更新和推送负载都会序列化整个结构体。
import 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"
}
}
}
过期日期
为 ActivityContent 设置 staleDate,以告知系统内容何时会过期。此日期过后,系统会将 context.isStale 设为 true;在视图中显示备用界面(例如“正在更新…”)。
let content = ActivityContent(
state: state,
staleDate: Date().addingTimeInterval(300), // stale after 5 minutes
relevanceScore: 75
)
活动生命周期
启动
使用 Activity.request 创建并显示实时活动。将 .token 作为 pushType 传入,以启用通过 APNs 进行的远程更新。
let 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)")
}
更新
从应用中更新动态内容状态。使用 AlertConfiguration 在更新的同时触发可见横幅和声音。
let 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)))
务必在所有代码路径中结束活动,包括成功、错误和取消。未清理的活动会一直留在锁定屏幕上,直到系统将其清除(最长 8 小时),这会让用户感到困扰。
锁定屏幕呈现
锁定屏幕是实时活动的主要显示界面。所有运行 iOS 16.1+ 的设备都会在此显示实时活动。请优先设计此布局。
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")
}
}
}
}
锁定屏幕尺寸
锁定屏幕呈现的垂直空间有限。避免使用高度超过约 160 点的布局。使用 supplementalActivityFamilies 选择 .small(紧凑)或 .medium(标准)尺寸:
ActivityConfiguration(for: DeliveryAttributes.self) { context in
// Lock Screen content
} dynamicIsland: { context in
// Dynamic Island
}
.supplementalActivityFamilies([.small, .medium])
灵动岛
iPhone 14 Pro 及后续机型支持灵动岛。它有三种呈现模式。请为所有模式进行设计,但应将锁定屏幕视为主要显示界面,因为并非所有设备都配备灵动岛。
紧凑模式(前导区域 + 尾随区域)
单个实时活动处于活跃状态时始终可见。可用空间极为有限——只显示最关键的信息。
| 区域 | 用途 |
|---|---|
compactLeading | 用于标识活动的图标或短标签 |
compactTrailing | 一个关键数值(计时器、分数、状态) |
最小模式
多个实时活动竞争显示空间时显示。只有一个活动能占用最小模式区域。显示单个图标或字形。
展开的区域
用户长按灵动岛时显示。
| 区域 | 位置 |
|---|---|
.leading | TrueDepth 摄像头左侧;向下延伸 |
.trailing | TrueDepth 摄像头右侧;向下延伸 |
.center | 摄像头正下方 |
.bottom | 所有其他区域下方 |
边缘色调
为灵动岛边框应用淡淡的色调:
DynamicIsland { /* expanded */ }
compactLeading: { /* ... */ }
compactTrailing: { /* ... */ }
minimal: { /* ... */ }
.keylineTint(.blue)
推送更新
推送更新通过 APNs 发送实时活动更新,比从应用轮询更高效,并且在应用暂停时也能正常工作。
设置
启动活动时,将 .token 作为 pushType 传入,然后将推送令牌转发到你的服务端:
let 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 负载格式
向 APNs 发送带有以下请求头和 JSON 正文的 HTTP/2 POST 请求:
必需的 HTTP 请求头:
apns-push-type: liveactivityapns-topic: <bundle-id>.push-type.liveactivityapns-priority: 5(低)或10(高,会触发提醒)
更新负载:
{
"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-state JSON 必须与 ContentState Codable 结构完全匹配。键或类型不匹配会导致静默失败。
推送启动
无需应用运行即可远程启动实时活动(iOS 17.2+):
Task {
for await token in Activity<DeliveryAttributes>.pushToStartTokenUpdates {
let tokenString = token.map { String(format: "%02x", $0) }.joined()
try await ServerAPI.shared.registerPushToStartToken(tokenString)
}
}
高频推送更新
将 NSSupportsLiveActivitiesFrequentUpdates = YES 添加到 Info.plist,以提高推送更新额度。适用于每分钟更新多次的活动(体育比分、乘车追踪)。
iOS 26 新增功能
计划的实时活动(iOS 26+)
安排实时活动在未来某个时间开始。即使应用未处于前台,系统也会自动启动活动。适用于已知开始时间的事件(体育比赛、航班、计划配送)。
let 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
)
ActivityStyle(iOS 16.1+ 类型,iOS 26+ 的 style: 参数)
控制持续时间:.standard(持续显示直至结束,默认值)或 .transient(系统可能自动关闭)。对交通到站等短暂更新使用 .transient。Activity.request 上的 style: 参数需要 iOS 26+。
let activity = try Activity.request(
attributes: attributes, content: content,
pushType: .token, style: .transient
)
Mac 菜单栏和 CarPlay(iOS 26+)
实时活动会自动显示在 macOS Tahoe 菜单栏(通过 iPhone 镜像)和 CarPlay 主屏幕中。无需额外代码——确保锁定屏幕布局在较小尺寸下仍清晰易读。
基于频道的推送(iOS 18+)
使用 .channel 同时向多个实时活动广播更新:
let activity = try Activity.request(
attributes: attributes, content: content,
pushType: .channel("delivery-updates")
)
常见错误
不要: 在紧凑展示中放入过多内容——它的空间很小。 应该: 紧凑展示的前导和尾随区域只显示最关键的信息(图标 + 一个数值)。
不要: 通过应用过于频繁地更新 Live Activities(会耗尽电量)。 应该: 对服务器驱动的更新使用推送更新。应用端更新仅限于用户操作。
不要: 忘记在事件完成时结束活动。 应该: 在成功、错误和取消路径中始终结束活动。遗留活动会让用户感到沮丧。
不要: 假设 Dynamic Island 始终可用(仅限 iPhone 14 Pro 及更新机型)。 应该: 将锁定屏幕作为主要界面进行设计;Dynamic Island 只是补充。
不要: 在 ActivityAttributes 中存储敏感信息(这些信息会显示在锁定屏幕上)。 应该: 将敏感数据保存在应用中,仅显示可安全展示的摘要。
不要: 忘记处理过期日期。
应该: 在视图中检查 context.isStale,并显示备用 UI(例如“正在更新……”)。
不要: 忽略推送令牌轮换。令牌随时可能变化。
应该: 使用 activity.pushTokenUpdates 异步序列,并在每次收到更新时重新注册。
不要: 忘记添加 NSSupportsLiveActivities Info.plist 键。
应该: 在宿主应用的 Info.plist 中添加 NSSupportsLiveActivities = YES(而不是扩展中)。
不要: 使用基于 contentState 的已弃用 API 来请求、更新或结束活动。
应该: 在所有生命周期调用中使用 ActivityContent。
不要: 在 Live Activity 视图中放入繁重逻辑。它们运行在尺寸受限的小组件进程中。
应该: 预先计算显示值,并通过 ContentState 传入。
审查清单
-
ActivityAttributes定义静态属性和ContentState - 宿主应用 Info.plist 中包含
NSSupportsLiveActivities = YES - 活动使用
ActivityContent(而非已弃用的 contentState API) - 所有代码路径中均已结束活动(成功、错误、取消)
- 锁定屏幕布局能够处理
context.isStale - 已实现 Dynamic Island 的紧凑、展开和最小布局
- 通过
activity.pushTokenUpdates将推送令牌转发到服务器 - 对重要更新使用
AlertConfiguration - 启动前检查
ActivityAuthorizationInfo -
ContentState保持精简(每次更新时都会序列化) - 已在设备上测试(Dynamic Island 与模拟器上的表现不同)
- 确保 ActivityAttributes 和 ContentState 类型符合 Sendable;在 @MainActor 上更新 Live Activity UI
参考资料
- 有关模式和代码示例,请参阅
references/live-activity-patterns.md