通过推送通知更新 Live Activities
Update Live Activities with push notifications
2023年6月5日
一句话判断
Live Activities 现在可以通过推送通知从服务端直接更新了——这意味着你的 App 不需要前台运行时间,不需要后台刷新,服务端就能驱动锁屏和灵动岛上的 UI 变化。
这场 Session 讲了什么
这场 Session 详细介绍了如何使用 APNs(Apple Push Notification service)远程更新 Live Activities。
核心工作流程分三步:
- 准备工作:App 在创建 Live Activity 时指定
pushType: .token,ActivityKit 向 APNs 请求一个 push token。App 通过pushTokenUpdatesasync sequence 监听 token 变化,并将 token 发送给服务端。 - 发送推送更新:服务端向 APNs 发送 HTTP/2 请求,headers 中指定
apns-push-type: liveactivity,payload 包含timestamp、event(update 或 end)、content-state(JSON 格式的内容状态)。 - 更新优先级与提醒:推送请求有 priority 5(低)和 10(高)两个级别。还可以通过
alert字段在更新 Live Activity 的同时发送锁屏提醒。
Session 用 Emoji Rangers 游戏的多人组队冒险功能作为示例——服务端追踪所有玩家的冒险状态,通过推送更新每个人的 Live Activity,App 端完全不需要前台运行。
关键技术细节:
- push token 是每个 Live Activity 独有的,一个 App 的多个 Live Activity 有不同的 token。
- token 可能会被系统更新,App 必须持续监听
pushTokenUpdates。 content-state的 JSON 编码必须使用默认的 JSONEncoder 策略,不能设置自定义的 key encoding strategy。- 推送更新可以附带
alert,包含标题、正文和默认声音,让用户注意到 Live Activity 的变化。
值得深挖的点
push token 的生命周期管理是个容易出错的环节。token 不仅在创建时获取,系统可能在 Live Activity 的整个生命周期内更新它。App 必须用 async for loop 持续监听,不能只在创建时读一次。新 token 出现时要及时通知服务端并作废旧 token。
timestamp 的作用不仅仅是时间戳。系统用它来判断哪个更新是最新的——如果你的服务端发了多个更新但到达顺序不确定,系统会用 timestamp 来保证最终渲染的是最新的内容状态。这意味着服务端的 timestamp 必须单调递增。
content-state 编码的坑:必须使用默认的 JSONDecoder/JSONEncoder 策略。如果你在 App 里用了 .convertToSnakeCase 之类的自定义策略,服务端发的 JSON 和系统解码用的 JSON 会不匹配,导致更新失败。Session 建议在 App 里用 Foundation 的 JSONEncoder 生成一份正确的 JSON 给服务端做参考。
开发调试的技巧很实用——可以直接用命令行工具向 APNs 发送推送请求,不需要搭完整的服务端。这大大加快了开发迭代速度。
代码片段
创建支持推送的 Live Activity 并监听 token:
// 创建 Live Activity 时指定 push 类型
let activity = try Activity.request(
attributes: adventureAttributes,
content: .init(state: initialState, staleDate: nil),
pushType: .token // 关键:请求推送 token
)
// 持续监听 push token 变化(不要只读一次)
Task {
for await tokenData in activity.pushTokenUpdates {
let token = tokenData.map { String(format: "%02x", $0) }.joined()
// 将 token 发送到你的服务端
await sendTokenToServer(token)
}
}
服务端推送请求的结构:
// APNs Headers
{
"apns-push-type": "liveactivity",
"apns-topic": "com.example.app.push-type.liveactivity",
"apns-priority": "10"
}
// APNs Payload
{
"timestamp": 1685976000, // 时间戳,系统据此判断最新状态
"event": "update", // "update" 或 "end"
"content-state": { // 编码为你的 ContentState 类型
"heroName": "Emoji Ranger",
"healthLevel": 0.8,
"status": "战斗中"
}
}
在 App 中生成 content-state JSON 参考:
// 在 App 中生成正确的 JSON 格式给服务端参考
let state = ContentState(heroName: "示例", healthLevel: 1.0, status: "待命")
let encoder = JSONEncoder()
let jsonData = try encoder.encode(state)
// 注意:不要设置自定义编码策略!
print(String(data: jsonData, encoding: .utf8)!)
// 把这个 JSON 结构给后端同事参考
带提醒的推送更新:
{
"timestamp": 1685976000,
"event": "update",
"content-state": {
"status": "冒险完成",
"score": 9500
},
"alert": {
"title": "冒险结束",
"body": "你的英雄完成了冒险,获得 9500 分!"
}
}
最佳实践
- push token 必须用 async for loop 持续监听,不要假设 token 只在创建时获取一次。系统可能在任何时候更新它。
- 服务端的 timestamp 必须严格单调递增。如果发了一个旧 timestamp 的更新,系统会忽略它。
- 开发阶段用命令行工具直接发推送请求来调试,效率比搭建完整后端高得多。
- content-state 的 JSON key 必须用 camelCase,因为系统用默认的 JSONDecoder 解码。不要和服务端的其他 API 混用 snake_case。
- 结束 Live Activity 时用
event: "end",不要发 update 后再在 App 端调用 end——那样会有竞态条件。 - 推送更新频率要有节制。Live Activity 不是实时数据流,过于频繁的更新会消耗电量和用户耐心。
- 保留 staleDate 机制作为推送失败的兜底——如果推送长时间未到达,系统可以显示内容已过时。
还有什么值得关注
apns-push-type: liveactivity只支持 token-based 的 APNs 连接,证书-based 连接不行。- 推送更新和本地 ActivityKit 更新可以混合使用——App 在前台时用本地更新更高效,后台时用推送更新。
- Session 没有深入讨论推送更新失败后的重试策略,但 APNs 本身有”best effort”语义,你的服务端应该有重试和兜底机制。
- Live Activity 最长持续 12 小时(iOS 16.2+),超过后系统会自动结束。推送更新无法延长这个时限。
- 灵动岛和锁屏上的 Live Activity 共享同一个推送更新机制,不需要分别处理。
staleDate字段可以在推送 payload 中设置,指定一个时间点——超过后系统会将内容标记为过时。