Update Live Activities with push notifications
System & Services 进阶 20m

通过推送通知更新 Live Activities

Update Live Activities with push notifications

2023年6月5日

在 Apple 官方观看视频

一句话判断

Live Activities 现在可以通过推送通知从服务端直接更新了——这意味着你的 App 不需要前台运行时间,不需要后台刷新,服务端就能驱动锁屏和灵动岛上的 UI 变化。

这场 Session 讲了什么

这场 Session 详细介绍了如何使用 APNs(Apple Push Notification service)远程更新 Live Activities。

核心工作流程分三步:

  1. 准备工作:App 在创建 Live Activity 时指定 pushType: .token,ActivityKit 向 APNs 请求一个 push token。App 通过 pushTokenUpdates async sequence 监听 token 变化,并将 token 发送给服务端。
  2. 发送推送更新:服务端向 APNs 发送 HTTP/2 请求,headers 中指定 apns-push-type: liveactivity,payload 包含 timestampevent(update 或 end)、content-state(JSON 格式的内容状态)。
  3. 更新优先级与提醒:推送请求有 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 中设置,指定一个时间点——超过后系统会将内容标记为过时。
WWDC 2023