文档
官方文档
官方文档-用户交互指南
官方文档-构建推送消息
主要参考文档
参考文档
开发须知
- 强烈建议阅读下官方文档
- Live Activity 包括两部分,灵动岛 和 锁定屏幕(通知中心),且必须对所有样式进行支持。
- 仅 iOS 16.1 以上支持,支持iPhone,iPad,详见官方用户交互指南文档
- ActivityKit用于管理Live Activities的生命周期。(request、update、end)
- 支持隐式动画
- 如果用户或者App没有主动终止Live Activity,灵动岛上最多可以存在8个小时,然后就会被系统终止。 如果在锁定屏幕上,它可以存活12小时
- Live Activity 使用的图片资源,要小于或等于Live Activity的大小。如果大于的话,可能无法启动。
- 每个Live Activity 有自己的沙盒
- 无法访问网络,无法使用定位功能。
- 可以通过 ActivityKit 或者 ActivityKit push notifications 来配置、启动、更新与终止 Live Activity但二者在更新时的动态数据大小均不能超过 4 KB
- 每个 Activity 对应一个推送token,Activity 中止后,令牌失效。
- 使用 ActivityKit push notifications 更新,每小时会有一定的预算,官方文档没具体说是多少。超过预算的消息有可能被系统限制。解决方案有两个,方案一是服务端在推消息的时候,手动指定消息优先级,消息优先级10(也是默认优先级)会被加入预算,消息优先级5不会被加入预算。方案二是在主工程的 info.plist 中加入 NSSupportsLiveActivitiesFrequentUpdates 并设为 YES,添加后会在 App 的设置里面,实时活动选项不再是开关,而是可以点进二级页,二级页里面会有 更频繁更新 的开关,用户可以随时关掉。这个选项也可以通过代码获取状态及监听状态变更。
- 可以通过Link实现点击不同区域,跳转到不同页面。如果只使用widgetURL的方式跳转,那么仅可以跳转到一个scheme页面。
- 锁屏小组件和灵动岛,共用一份数据,更新时同步更新。可以设计为不同的UI样式。
开发流程
- 创建 Widget Extension,并确保 Include Live Activity是勾选上的
- 在主工程中添加 NSSupportsLiveActivities 并设为 YES
- 如需要更频繁的推送,在主工程中添加 NSSupportsLiveActivitiesFrequentUpdates 并设为 YES
- 在主工程中,点击Project - Target - Signing & Capabilities,确认添加了 Push Notificatioins 功能
- 创建好 Widget,会自动生成模版代码,只需要在上面修改相应代码即可
// 入口代码,确定需要加载 Live Activity
@main
struct PizzaDeliveryWidgets: WidgetBundle {
var body: some Widget {
// widget
FavoritePizzaWidget()
// Live Activity
if #available(iOS 16.1, *) {
PizzaDeliveryLiveActivity()
}
}
}
// 配置数据
struct PizzaDeliveryAttributes: ActivityAttributes {
public typealias PizzaDeliveryStatus = ContentState
// 动态数据
public struct ContentState: Codable, Hashable {
var driverName: String
var deliveryTimer: ClosedRange<Date>
}
// 静态数据
var numberOfPizzas: Int
var totalAmount: String
var orderNumber: String
}
// 配置UI,共计需要设计出 4 个 UI,且全都需要实现
struct PizzaDeliveryActivityWidget: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: PizzaDeliveryAttributes.self) { context in
// 锁屏UI,出现在所有设备上 - 第一个UI
// 不支持灵动岛的未锁屏的设备上,显示为 banner UI
// 两个都使用同一个View组件,在这里配置
// 系统使用默认的文本颜色和最适合锁定屏幕的实时活动背景色
VStack {
Text("Your \(context.state.driverName) is on the way!")
}
.activityBackgroundTint(Color.cyan) // 修改背景颜色
.activitySystemActionForegroundColor(Color.black) // 修改文本颜色
} dynamicIsland: { context in
// 灵动岛UI
DynamicIsland {
// 展开后的UI - 第二个UI
// 需要组合不同的区域
DynamicIslandExpandedRegion(.leading) {
// 展开后的前面
Text("Leading")
}
DynamicIslandExpandedRegion(.trailing) {
// 展开后的后面
Text("Trailing")
}
DynamicIslandExpandedRegion(.center) {
// 展开后的中间
Text("Center")
}
DynamicIslandExpandedRegion(.bottom) {
// 展开后的底部
Text("Bottom")
}
} compactLeading: {
// 紧凑样式的左边 - 第三个UI(左)
Text("L")
} compactTrailing: {
// 紧凑样式的右边 - 第三个UI(右)
Text("T")
} minimal: {
// 当有多个 Live Activity时,灵动岛显示成circular minimal 样式 - 第四个UI
Text("Min")
}
.widgetURL(URL(string: "demo://homepage")) // 点击跳转到指定页面
.keylineTint(Color.red)
}
}
}
- 修改主项目工程文件,配置、启动、更新与终止 Live Activity
// 启动 Live Activity
func startDeliveryPizza() {
// 判断版本号
guard #available(iOS 16.1, *) else {
return
}
// 判断是否开启了 Live Activity 权限
guard ActivityAuthorizationInfo().areActivitiesEnabled else {
// Live Activity 不可用,上报空 token 给服务端
uploadTokenToService(nil)
return
}
// 创建数据
let pizzaDeliveryAttributes = PizzaDeliveryAttributes(numberOfPizzas: 1, totalAmount:"$99")
let initialContentState = PizzaDeliveryAttributes.PizzaDeliveryStatus(driverName: "TIM 👨🏻🍳", estimatedDeliveryTime: Date()...Date().addingTimeInterval(15 * 60))
do {
// 请求启动 Live Activity
let deliveryActivity = try Activity<PizzaDeliveryAttributes>.request(
attributes: pizzaDeliveryAttributes,
contentState: initialContentState,
pushType: .token) // Enable Push Notification Capability First (from pushType: nil)
print("Requested a pizza delivery Live Activity \(deliveryActivity.id)")
Task {
// 监听 push token 更新
for await pushToken in deliveryActivity.pushTokenUpdates {
let pushTokenString = pushToken.reduce("") { $0 + String(format: "%02x", $1) }
print(pushTokenString)
// 上传 push token 给服务端,用于推送更新 Live Activity
uploadTokenToService(pushTokenString)
}
}
Task {
// 监听 state 数据内容变化
for await state in deliveryActivity.contentStateUpdates {
print("1content state update: tip=\(state.driverName)")
}
}
Task {
// 监听 Activity 状态变化
for await state in deliveryActivity.activityStateUpdates {
print("activity state update: tip=\(state) id:\(deliveryActivity.id)")
// 当 LiveActivity 结束时,使服务端的推送token失效
// LiveActivity 活动状态一共有 4 种
// .active 处于活动中
// .ended 已经终止且不会有任何更新,但依旧在锁屏界面展示
// .dismissed 结束且不再展示
// .stale 消息过时,等待最新的消息。(iOS 16.2 以上才支持)
if state == .ended || state == .dismissed {
uploadTokenToService(nil)
}
}
}
} catch (let error) {
print("Error requesting pizza delivery Live Activity \(error.localizedDescription)")
// Live Activity 不可用,上报空 token 给服务端
uploadTokenToService(nil)
}
}
// 更新 Live Activity
func updateDeliveryPizza() {
// 判断版本号
guard #available(iOS 16.1, *) else {
return
}
Task {
// 获取数据
let updatedDeliveryStatus = PizzaDeliveryAttributes.PizzaDeliveryStatus(driverName: "TIM 👨🏻🍳", estimatedDeliveryTime: Date()...Date().addingTimeInterval(60 * 60))
// 更新数据
for activity in Activity<PizzaDeliveryAttributes>.activities{
// 用户可以在锁定屏幕上移除Live Activity 后,ActivityState会变为.dismissed。
if activity.activityState == .dismissed {
continue
}
await activity.update(using: updatedDeliveryStatus)
}
}
}
// 结束 Live Activity
func stopDeliveryPizza() {
// 判断版本号
guard #available(iOS 16.1, *) else {
return
}
Task {
// dismissalPolicy 有三种
// .default 会在锁屏屏幕上停留四个小时,以便用户查看最后一个消息,或用户主动移除
// .immediate 立即结束,不会在屏幕上停留
// .after() 指定时间结束,最长为当前时间+4小时
for activity in Activity<PizzaDeliveryAttributes>.activities{
// 用户可以在锁定屏幕上移除Live Activity 后,ActivityState会变为.dismissed。
if activity.activityState == .dismissed {
continue
}
await activity.end(dismissalPolicy: .immediate)
}
print("Cancelled pizza delivery Live Activity")
}
}
// 展示所有 Live Activity
func showAllDeliveries() {
// 判断版本号
guard #available(iOS 16.1, *) else {
return
}
Task {
for activity in Activity<PizzaDeliveryAttributes>.activities {
print("Pizza delivery details: \(activity.id) -> \(activity.attributes)")
}
}
}
推送更新数据
- 主工程开通远程推送功能
- 主工程启动 LiveActivity,获取 pushToken,且监听 pushToken 变化,获取变化后的pushToken
- 将 pushToken 通过API上报给服务端
- 服务端设置推送消息的Header header field,将 apns-push-type 字段的值设置为liveactivity,将 apns-topic 字段的值设置为 <your bundleID>.push-type.liveactivity
- 服务端设置推送消息的 json,"aps" 字段下的 "content-state" 字段,要和客户端 ActivityAttributes 的 ContentState 中定义的动态数据字段名相对应
- 服务端设置推送消息的 json,"aps" 字段下的 "events",设置为 "update" 或 "end"。如果是end,需要确保 content-state 数据为最终的数据状态,因为之后LiveActivity就不可以再更新了
- 如果主App存活期间, LiveActivity 被标记为结束时,需要调用接口,将服务端的 pushToken 置为失效。如果是服务端推送 end 状态,将 LiveActivity 置为结束,需要服务端主动将 pushToken 置为失效
- 扩展 - 可在 "aps" 下设置 "stale-date" 字段,确保 LiveActivity 不会展示过时的消息内容。例如,用户断网,没有收到最新的 update 推送消息,如果到达了 "stale-date" 的时间,LiveActivity 的状态会自动变为 stale,显示消息已过时的 UI,等最新的消息到达后,或用户主动操作后,再更新 UI 为其他的状态。但 stale 状态支持需 iOS 16.2 以上
- 扩展 - 可在 "aps" 下设置 "dismissal-date" 字段,修改默认的 end 状态停留时长。如果不设置此字段,且结束时设置的 dismissalPolicy 为 .default,那么结束状态的 UI 会在锁屏界面默认停留4个小时,除非用户主动移除它。
// 官方提供的推送消息内容示例
{
"aps": {
"timestamp": 1168364460,
"events": "update",
"relevance-score": 75.0,
"stale-date": 1650998941,
"content-state": {
"driverName": "Anne Johnson",
"estimatedDeliveryTime": 1659416400
},
"alert": {
"title": "Delivery Update",
"body": "Your pizza order will arrive soon.",
"sound": "example.aiff"
}
}
}
常见问题
The operation couldn’t be completed. (com.apple.ActivityKit.ActivityInput error 1.)
// 检查主工程的info里面,是否添加了NSSupportsLiveActivities并设置为YES
The operation couldn’t be completed. (com.apple.ActivityKit.ActivityInput error 0.)
// 检查主工程,是否添加了推送功能。Project - Target - Signing & Capabilities,如果没有 Push Notificatioins,则点击 + 添加