关于 activitykit
ActivityKit 是用于实现锁定屏幕和 Dynamic Island 实时活动的开发技能,面向 iOS 开发者。它涵盖状态与生命周期、界面呈现及 APNs 推送更新,并指导验证负载格式。
主要功能
- 01定义静态属性和动态状态
- 02创建、更新和结束活动
- 03设计锁定屏幕与 Dynamic Island 布局
- 04通过 APNs 发送更新
- 05处理定时启动与频道推送活动
适用场景
- 01在锁定屏幕显示配送进度
- 02在 Dynamic Island 显示实时比分
- 03根据服务器信息更新活动状态
- 04为已知时间的事件安排活动
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dpearson2699/ swift-ios-skills --skill activitykit安装后在 Claude Code 中输入/activitykit确认已加载。
附带文件evals/evals.jsonreferences/activitykit-patterns.md
详细介绍
ActivityKit
ActivityKit 管理在锁定屏幕和灵动岛上显示的实时、一目了然的 Live Activities。普通时间线小组件应放在 widgetkit 中,通用 APNs 配置应放在 push-notifications 中;ActivityKit 管理 Live Activity 生命周期和载荷契约。aps.content-state 必须解码为与 ActivityAttributes.ContentState 完全一致的结构,包括任何协同使用的自定义日期/范围编码。除非另有说明,现代 ActivityContent 生命周期示例要求 iOS 16.2+。
请参阅 references/activitykit-patterns.md,了解完整代码模式,包括推送载荷格式、并发活动、状态观察和测试。
目录
工作流
1. 创建新的 Live Activity
- 确认宿主应用的功能配置以及
NSSupportsLiveActivities = YES。 - 定义
ActivityAttributes.ContentState;对符合服务端载荷契约的代表性样例进行编码和解码。 - 创建
ActivityConfiguration,并预览锁定屏幕和灵动岛上的状态,包括过期内容和终止内容。 - 检查
ActivityAuthorizationInfo.areActivitiesEnabled,然后请求并观察活动生命周期。 - 测试本地更新和所有终止路径。
- 对于远程更新,在向服务端注册轮换更新令牌或推送启动令牌之前,先验证完整的参考载荷。
2. 审查现有 Live Activity 代码
按照本文末尾的审查清单逐项检查。
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 生命周期
启动
使用 Activity.request 创建并显示 Live Activity。将 .token 作为 pushType 传入,以启用通过 APNs 进行的远程更新。此处展示的 ActivityContent 请求要求 iOS 16.2+。
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 小时,除非应用或用户提前结束)、由系统结束后在锁定屏幕上的显示时间(额外最多 4 小时,从开始算起总计 12 小时),以及由应用结束后 .default 策略的消除等待时间(结束后最多 4 小时)。
锁定屏幕展示
锁定屏幕是 Live Activity 的主要显示界面。每台运行 iOS 16.1+ 的设备都会在此显示 Live Activities。请先设计此布局,再根据需要适配灵动岛。
struct DeliveryActivityWidget: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: DeliveryAttributes.self) { context in
VStack(alignment: .leading) {
Text(context.attributes.restaurantName).font(.headline)
if context.isStale {
Label("Updating...", systemImage: "arrow.trianglehead.2.clockwise")
.foregroundStyle(.secondary)
} else {
Text(timerInterval: context.state.estimatedDeliveryTime, countsDown: true)
.monospacedDigit()
}
}
.padding()
} dynamicIsland: { context in
DynamicIsland {
DynamicIslandExpandedRegion(.center) {
Text(context.attributes.restaurantName).font(.headline)
}
DynamicIslandExpandedRegion(.trailing) {
Text(timerInterval: context.state.estimatedDeliveryTime, countsDown: true)
}
} compactLeading: {
Image(systemName: "box.truck.fill")
} compactTrailing: {
Text(timerInterval: context.state.estimatedDeliveryTime, countsDown: true)
} minimal: {
Image(systemName: "box.truck.fill")
}
}
}
}
补充活动布局系列
锁定屏幕的垂直空间有限。避免使用高度超过大约 160 点的布局。在 iOS 18 及更高版本中,如果要提供默认布局之外的自适应布局,请使用 supplementalActivityFamilies:.medium 用于 iOS/macOS 实时活动尺寸,.small 用于 watchOS 实时活动尺寸。
ActivityConfiguration(for: DeliveryAttributes.self) { context in
// Lock Screen content
} dynamicIsland: { context in
// Dynamic Island
}
.supplementalActivityFamilies([.medium, .small])
灵动岛
灵动岛展示仅会出现在配备灵动岛的设备上。请设计全部三种模式,但将锁定屏幕视为主要界面,因为并非所有设备都配备灵动岛。
紧凑模式(前导 + 尾随)
当一项实时活动占用灵动岛紧凑空间时使用。可用空间极其有限——只显示最关键的信息。
| 区域 | 用途 |
|---|---|
compactLeading | 用于标识活动的图标或极短标签 |
compactTrailing | 一个关键值(计时器、分数、状态) |
最小模式
当多项实时活动争用空间时显示。只有一项活动能占用最小模式区域。显示单个图标或字形。
展开区域
用户长按灵动岛时显示。
| 区域 | 位置 |
|---|---|
.leading | TrueDepth 摄像头左侧;可向下换行 |
.trailing | TrueDepth 摄像头右侧;可向下换行 |
.center | 摄像头正下方 |
.bottom | 所有其他区域的下方 |
关键线颜色
为灵动岛边框应用微妙的色调:
DynamicIsland { /* expanded */ }
compactLeading: { /* ... */ }
compactTrailing: { /* ... */ }
minimal: { /* ... */ }
.keylineTint(.blue)
推送更新
推送更新通过 APNs 发送实时活动更新,比应用轮询更高效,并且在应用暂停时也能工作,但受 APNs 送达、优先级、预算和限流机制影响。
设置
启动活动时,将 pushType 设为 .token,然后将每个活动的更新令牌转发到你的服务端。更新令牌可能会轮换,因此请监听 activity.pushTokenUpdates,并重新注册每个发出的令牌:
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 负载格式
使用以下标头和 JSON 正文向 APNs 发送 HTTP/2 POST 请求:
必需的设备令牌 HTTP 标头:
apns-push-type: liveactivityapns-topic: <bundle-id>.push-type.liveactivityapns-priority: 5(较低优先级)或10(立即发送,会计入预算)
aps.alert 负载控制可见提醒、横幅和声音行为;仅设置优先级不会触发提醒。
将 timestamp、event 和完整的 content-state 放在 aps 中。请根据推送更新负载中的完整示例,验证更新、结束和推送启动正文,包括准确的 Codable 日期/范围表示形式。
推送启动
无需运行应用即可远程启动实时活动(iOS 17.2 及更高版本)。推送启动令牌是来自 Activity<Attributes>.pushToStartTokenUpdates 的 ActivityKit 专用令牌,与普通应用/设备 APNs 令牌以及每个活动的更新令牌不同:
Task {
for await token in Activity<DeliveryAttributes>.pushToStartTokenUpdates {
let tokenString = token.map { String(format: "%02x", $0) }.joined()
try await ServerAPI.shared.registerPushToStartToken(tokenString)
}
}
高频推送更新
在 Info.plist 中添加 NSSupportsLiveActivitiesFrequentUpdates = YES,以增加系统管理的推送更新预算。当更新频率很重要时,请检查 ActivityAuthorizationInfo.frequentPushesEnabled 并监听 frequentPushEnablementUpdates;Apple 不保证固定的更新速率。
最新功能
计划的实时活动(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,
style: .standard,
alertConfiguration: AlertConfiguration(
title: "Game Starting",
body: "The live score is ready.",
sound: .default
),
start: scheduledDate
)
ActivityStyle(iOS 18 及更高版本的请求参数)
使用 iOS 18 及更高版本的 style: 请求参数选择持久化行为。配送、出行、体育比分、计时器和航班/状态看板等持久实时活动,请使用 .standard。仅在短暂展示展开的灵动岛时使用 .transient;当用户锁定设备、收起或缩小展开的展示、离开应用,或在灵动岛之外执行其他操作时,它可能会自动结束。
let activity = try Activity.request(
attributes: attributes,
content: content,
pushType: .token,
style: .standard
)
配对的 Mac 与 CarPlay(iOS 26+)
实时活动可以显示在配对的 Mac 和 CarPlay 主屏幕上。无需额外的 ActivityKit API,但请验证紧凑布局;实时活动中的按钮和开关在 CarPlay 中不会执行操作。
基于频道的推送(iOS 18+)
使用 APNs 创建的频道 ID,一次向多个实时活动广播更新。请在 Xcode 之外启用广播功能,在服务端创建频道,然后使用 .channel(channelID) 订阅。频道推送可以更新或结束实时活动,但不能启动实时活动。对于频道推送,请使用 apns-channel-id 和过期时间,而不是上文设备令牌示例中的 apns-topic。
let activity = try Activity.request(
attributes: attributes, content: content,
pushType: .channel(channelIDFromServer)
)
常见错误
不要: 在紧凑显示中放入太多内容——空间很小。 要: 仅在紧凑显示的前导/尾随区域展示最关键的信息(图标 + 一个数值)。
不要: 在应用中更新实时活动过于频繁(会消耗电池电量)。 要: 对由服务端驱动的更新使用推送更新。将应用端更新限制在用户操作时。
不要: 忘记在事件进入任何终止状态时结束活动。 要: 在成功、取消、退出登录、不可恢复的错误以及服务端终止性故障时结束活动。未结束的活动会让用户感到困扰。
不要: 假设每台设备都有灵动岛。 要: 将锁定屏幕作为主要显示界面进行设计;灵动岛是辅助显示界面。
不要: 在 ActivityAttributes 中存储敏感信息(这些信息会显示在锁定屏幕上)。 要: 将敏感数据保留在应用中,只显示可安全展示的摘要。
不要: 忘记处理过期日期。
要: 在视图中检查 context.isStale,并显示备用界面(例如“正在更新……”)。
不要: 忽略推送令牌轮换。令牌随时可能变化。
要: 使用 activity.pushTokenUpdates 异步序列,并在每次收到新值时重新注册。
不要: 忘记添加 NSSupportsLiveActivities Info.plist 键。
要: 将 NSSupportsLiveActivities = YES 添加到宿主应用的 Info.plist(而不是扩展)。
不要: 在请求/更新/结束时使用已弃用的基于 contentState 的 API。
要: 在所有生命周期调用中使用 ActivityContent。
不要: 直接从实时活动视图获取网络数据或位置信息。 要: 在应用或服务端预先计算显示值,并通过 ActivityKit 更新或推送传递这些值。
审查清单
-
ActivityAttributes定义了静态属性和ContentState - 宿主应用的 Info.plist 中设置了
NSSupportsLiveActivities = YES - Activity 使用
ActivityContent(而非已弃用的 contentState API) - 在所有终止路径(成功、错误、取消、退出登录、服务端终止性故障)中结束 Activity
- 将 ActivityKit 生命周期以及锁定屏幕/灵动岛上的实时活动界面,与普通主屏幕/时间线小组件的工作区分开
- 锁定屏幕布局(实时活动的主要界面)处理了
context.isStale - 实现了灵动岛的紧凑、展开和最小模式,并提供锁定屏幕回退界面
- 通过
activity.pushTokenUpdates将推送更新令牌转发给服务端 - 通过
Activity<Attributes>.pushToStartTokenUpdates收集推送启动令牌 - 推送启动载荷包含必需的
alert -
content-stateJSON 与实际的ContentStateCodable结构相匹配,包括协调一致的日期/范围编码 - 审查时区分 8 小时的活动时长、系统结束后在锁定屏幕上总计显示 12 小时,以及应用结束后
.default模式的 4 小时保留时间 - 启动前检查
ActivityAuthorizationInfo - 检查
frequentPushesEnabled,再判断是否支持高频推送 - 保持
ContentState简洁(每次更新都会对其序列化) - 对 iOS 18+ 的
style:、.channel和补充系列进行可用性检查 - 合理选择 iOS 18+ 的
style::持久型实时活动使用.standard,仅在灵动岛展开界面短暂显示时使用.transient - 分别处理 ActivityKit 推送优先级和
aps.alert行为 - 实时活动视图避免直接执行网络/定位操作
- 在设备上测试推送送达情况和灵动岛行为
参考资料
- 模式和代码示例请参阅 references/activitykit-patterns.md