大多数浏览器和
Developer App 均支持流媒体播放。
-
通过推送通知更新实时活动
了解在使用 Apple 推送通知服务 (APN) 推送内容时,如何远程更新 App 中的实时活动。我们将向你展示如何在本地配置第一个实时活动推送,以便你可以快速迭代实现内容。了解确定推送优先级和配置提醒更新的最佳实践,探索如何利用相关性分数和过时日期进一步改进实时活动。为了让本次讲座发挥最大价值,你需要提前了解 ActivityKit 和实时活动。请观看“了解 ActivityKit”,了解实时活动的介绍
章节
- 0:00 - Intro
- 2:10 - Preparations
- 5:58 - First push update
- 11:04 - Priority and alerts
- 15:40 - Enhancements
- 17:27 - Wrap-up
资源
- ActivityKit
- Establishing a token-based connection to APNs
- Human Interface Guidelines: Live Activities
- Sending notification requests to APNs
- Sending push notifications using command-line tools
- Starting and updating Live Activities with ActivityKit push notifications
相关视频
WWDC24
WWDC23
-
下载
♪ ♪
Jeff:大家好 我是 Jeff 是 Live Activities 团队的一名工程师 很高兴能够与你分享 如何使用推送通知 来更新实时活动 实时活动可有效地向他人展示 有关当前进行中活动的信息 而且一目了然 ActivityKit 能够让 App 启动、更新和结束实时活动 其次 使用 WidgetKit 和 SwiftUI 你就可以构建 向用户展示信息的 UI
如果你想进一步了解 有关这些技术的信息 请观看 Can 的讲座 “了解 ActivityKit” 在讲座“了解 ActivityKit”中 Can 在 Emoji Rangers App 添加了新的实时活动 来显示英雄冒险的状态 但我认为英雄要是 有同伴的话会更有趣 所以 我想添加一个新功能 可以让多个用户使用各自的英雄 进行组队并共同完成冒险 为了提供最佳的用户体验 我将更新实时活动 来显示队伍中所有的英雄事件
为了实现这一点 我会引入一个服务器 来对冒险进行跟踪 而不是在设备上进行此工作 服务器会负责将实时活动 保持为最新状态 并且 由于计算 是在服务器上完成的 App 就不需要前台运行时 来更新实时活动 从而可以减少 对用户电池续航时间的影响 我认为 使用 ActivityKit 推送通知 来更新实时活动 是实现这些功能的好方法 在本次讲座中 我会先向你介绍使用推送通知 更新实时活动所需的准备工作 接着 我会向你演示 如何从计算机发送第一个推送更新 然后 我会介绍 更新优先级之间的差异 以及如何通知用户 最后 我会向你介绍其他 可用于推送更新的增强操作 使其提升到更高的水平 先从准备工作开始介绍 在你使用推送通知 更新实时活动之前 有必要先了解 App 和服务器如何 与 Apple 推送通知服务进行交互 先从 App 开始 在启动新的实时活动后 ActivityKit 就会 从 Apple 推送通知 (简称 APN) 服务中 获取一个推送令牌 该推送令牌对于你所请求的 每个实时活动都是唯一的 因此 你的 App 需要 在发送推送更新前 将该令牌发送给服务器 接着 每当你需要更新实时活动时 服务器就会使用令牌 将推送请求发送给 APNs 最后 APNs 会将有效负载 发送给设备 并唤醒你的小组件扩展来渲染 UI
为了支持该新功能 APNs 引入了 一种新的 liveactivity 推送类型 该推送类型 只适用于与 APNs 建立了 基于令牌连接的服务器 想要进一步了解 有关发送推送请求的信息 请参阅文档 “向 APNs 发送通知请求” 想要了解更多 有关基于令牌连接的信息 请参阅文档 “与 APNs 建立基于令牌的连接” 下一步就是对 App 进行修改 并将实时活动配置为接收推送更新 在 Xcode 中 转到 App target 在“Signing & Capabilities”标签页中 添加推送通知功能 这样 ActivityKit 就可以 代表你的 App 请求推送令牌 现在 我来深入介绍一下代码 这里是 Emoji Rangers 用于请求实时活动的部分代码 我在 Activity.request 方法中 传入了 adventure 属性 和初始内容状态 为了支持接收推送更新 我在该方法中添加了 pushType 参数 并将其值设置为“token” 这样 ActivityKit 就会知道 要在创建实时活动时 请求推送令牌 在创建完活动后 你的 App 就需要向服务器 发送推送令牌 Activity 类型中的 pushToken 属性 可以让你同步访问推送令牌 但是 请不要在活动创建完后 立刻对其进行访问 大部分情况下 你获取的值会是 nil 这是因为请求推送令牌 是一个异步过程 同时 系统在活动的整个生命周期中 随时都有可能更新推送令牌 所以 你的 App 需要 相应地处理该问题 正确处理推送令牌的方法 是先创建一个异步 Task 然后启动 for-await 循环 来观察来自活动的 pushTokenUpdates 异步序列值 for 循环内的代码 会在出现新的 实时活动推送令牌时执行 务必在这里使用异步 for 循环 因为其不仅可以处理第一次推送令牌 还能处理后续的推送令牌更新 在收到令牌后 将其转换为十六进制字符串 并记录到调试控制台 这一点将在下一部分的 测试中派上用场 最后 将推送令牌 和其他 App 所需的数据 一起发送到服务器 由于每个活动的令牌都是唯一的 因此 务必对用户启动的 每个实时活动进行跟踪 同时 在系统请求现有活动的 新推送令牌时 你的 App 将会获取前台运行时 以进行相应处理 请务必将新的推送令牌发送到服务器 并使旧的令牌失效 因为这样才能确保 后续的推送更新正确发送 至此 准备工作已经就绪 你可以发送第一个推送更新了 要发送推送更新 你必须向 APNs 发送 HTTP 请求 该请求由两部分组成: APNs 标头和 APNs 有效负载 除了常用的 HTTP 标头 你还需要提供 3 个标头 第一个是 apns-push-type 其值为 liveactivity 接下来是 apns-topic 这是 App 的 BUNDLE_ID 后跟 .push-type.liveactivity 第三个是 apns-priority 其值可以是 5 或 10 5 表示推送请求的优先级低 而 10 表示优先级高 我会在测试期间使用高优先级 因为这样实时活动就可以立即更新 对于第一个 APNs 有效负载 你需要发送包含三个字段的有效负载 第一个是“timestamp” 这是自 1970 年以来的秒数 系统会使用 timestamp 来确保其始终呈现 最新的内容状态 第二个是“event” 表示需要在实时活动中执行的操作 其值可以是“update” 也可以是“end” 对于该初始 APNs 请求 其值应设置为“update” 第三个字段是“content-state” 该字段是可以解码为活动的 内容状态类型的 JSON 对象 为确保以正确的格式获取内容状态 你可以在 App 中使用 Foundation 的 JSONEncoder 类型 在这里 我创建了一个实时活动 ContentState 的实例 然后 对 JSONEncoder 进行实例化 最后 我会将内容状态 编码为 JSON 数据 并将其字符串 表示形式记录到控制台 这个使用驼峰式键值的 JSON 输出 看起来和我预期的一样 你的内容状态 JSON 会始终使用 JSONDecoder 和默认的解码策略进行解码 因此 在对内容状态进行编码时 请不要设置任何自定义编码策略 否则 你的 JSON 会无法匹配编码策略 然后 系统就无法 对你的实时活动进行更新 现在 你已经了解了 推送请求中包含的内容 下一步就是发送一个推送请求 来进行测试 在开发过程中 我非常喜欢进行快速迭代 因此 我喜欢在无需修改 服务器的情况下 测试实时活动的推送通知 为了实现这一点 我可以直接从终端向 APNs 发送推送请求 如果你需要通过 设置命令行来实现该操作 请参阅文档 “使用命令行工具发送推送通知” 你需要确保按照讲座 “使用令牌发送推送通知”中的 说明进行操作 通过打印身份验证令牌变量 你可以快速验证 是否已正确设置所有内容 接下来 你需要的信息是推送令牌 在前面的部分中 我添加了负责 将推送令牌记录到控制台的代码 所以 我可以从这里获取推送令牌 如果你采用了相同的方法 请继续将你的 App 部署到设备上并启动实时活动 你的 App 就会在活动开始 不久后记录推送令牌 复制推送令牌 并在终端中将其设置为活动推送 令牌变量 要发送 APNs 请求 你需要执行 curl 命令 这里展示的是 我为冒险实时活动 构建的 curl 命令 将“apns-topic”标头设置为 App 的 BUNDLE ID 后跟推送类型后缀 接着 将“apns-push-type”标头 设置为“liveactivity” 然后 将“apns-priority” 设置为 10 这样 我的请求可以 被立即发送 最后 将 HTTP 标头 “authorization” 设置为“bearer” 后跟身份验证令牌变量 在 data 部分 其包含了所有 APNs 有效负载 我使用 date 命令 自动创建时间戳 从而保证数字 可以精确到秒 最后 对于 URL 你需要确保使用 HTTP2 在 URL 的末尾 我引用了此前步骤中 设置的活动推送令牌变量 这样就完成了 在你执行此 curl 命令时 你的实时活动就会使用 此前有效负载提供的 新内容状态进行更新 有时 你可能会遇到实时活动 未按预期进行更新的情况 首先 你需要确保 在执行 curl 命令时 没有出现错误响应 出现错误可能表明 请求中有错误的字段 或是在设置环境时存在问题 如果 APNs 返回成功的响应 但你的实时活动却未更新 那你可以使用控制台 App 来查看设备日志 并对问题进行诊断 可能具有相关日志的进程包括 liveactivitiesd、 apsd 和 chronod 对使用推送通知更新 实时活动的方式感到满意后 你就可以开始修改服务器 并发送真实的推送更新了 接下来 我们将进入 设计用户体验的关键部分: 优先级和提醒 为了确保提供最佳用户体验 请务必为每个更新 选择恰当的推送优先级 你要考虑使用的优先级 应始终为低优先级 低优先级的更新 会根据当前条件进行传送 从而可以减少 对用户电池续航时间的影响 但是 这就意味着发送推送请求时 实时活动可能无法立即更新 因此 你应该对时间敏感度较低的 更新使用低优先级 对于冒险实时活动 例如 找到常见战利品 以及使英雄恢复生命值等 这些更新不需要用户即时的注意 所以 这些更新非常适合 使用低优先级 使用低优先级的另一个好处是 发送更新的数量没有限制 为了充分利用该优势 你可以对大部分的 实时活动更新使用低优先级 另一方面 某些更新 需要用户的即时注意 例如 英雄被击倒 或超级恶霸被击败时 在这些情况下 我会选择高优先级更新 高优先级更新可以立即传送 所以 它非常适用于 时间敏感度较高的更新 但是 因为此类更新 会影响用户电池续航时间 所以 统会根据设备状况设定预算 如果你的 App 超出了预算 系统就会限制推送更新 这样会大大影响用户体验 你最了解自己的 App 所以 你需要仔细考虑 每个更新使用的优先级 在 Emoji Rangers 我引入了一种特殊类型的冒险 其中 团队需要连续击败超级恶霸 为了给这种密集的实时活动 带来最佳的用户体验 我需要服务器频繁发送 高优先级的推送 以保持最新状态 为了支持这一点 我会启用 App 的实时活动 频繁更新功能 启用该功能后 就可以 让我的 App 获取更高的更新预算 从而降低实时活动更新 被限制的可能性 为了采用该功能 我只需要 在 Info.plist 中添加新的键值 NSSupportsLiveActivitiesFrequentUpdates 并将其值设置为 YES 用户可以在设置中 对单个实时活动禁用频繁更新 所以 你可以访问 ActivityAuthorizationInfo(). frequentPushesEnabled 属性 来检测频繁更新功能的状态 你的服务器应根据该属性值 调整其更新频率 所以 在服务器发送推送更新前 务必确保将该属性值发送到服务器 在活动开始后 你只需要检查一次该属性值即可 如果该属性值发生变化 系统就会结束所有正在进行的活动 所以 你的服务器无需担心频繁更新 在活动生命周期内会被切换 在冒险实时活动中 当英雄被击倒时 除了立即更新 我还希望引起用户的注意 以便他们可以迅速进入 App 使用治疗药水 为了实现这一点 我会向有效负载中的 “alert”对象添加 3 个字段 “title”是通知的标题 “body”是有关更新的简短消息 “sound”会指示触发提醒时 播放的声音 Emoji Rangers 支持多种语言 所以 只发送英文的提醒并不完美 但是 在服务器上 处理本地化非常困难 好在还有另一种方法可以设置 提醒对象中的 “title”和“body”字段 相较于传入字符串 我会将该字段 设置为本地化字符串对象 “loc-key”字段是可以 在 App 本地化文件中 找到的本地化键值 “loc-args”字段 是插入到本地化字符串中的 值列表 现在 设备就可以根据用户的位置 自动对通知进行本地化了 在对提醒进行最后的修改时 我想为不同的更新添加自定义声音 为了实现这一点 首先 我需要将声音文件 作为资源添加到 App 的目标中 接着 将提醒对象的“sound”字段 设置为声音的文件名 这样就完成了 现在 提醒的外观和声音都很棒了 接下来 我会进行一些改进 来真正提升实时活动的用户体验 在冒险结束后 我想隔一段时间 再结束实时活动并将其解除 为了实现这一点 我会发送一个 event 设为 end 的推送有效负载 我提供了 自定义“dismissal-date” 因为我想控制实时活动 从锁屏中移除的时间 你也可以忽略该字段 让系统来决定 解除实时活动的时间 “dismissal-date”的值 是自 1970 年以来的秒数 我还提供了最终的 content-state 来为实时活动提供最后的更新 该字段也是可选的 如果你选择忽略 活动就会 继续展示此前的内容状态 直到其被解除为止 有时候 用户的设备 无法接收推送通知 并且 冒险实时活动可能显示 过时的健康值 在此类情况下 我想在实时活动 UI 中 提醒用户 当前活动 可能会显示不准确的信息 为了实现这一点 我在有效负载中 添加了“stale-date”字段 系统会使用该日期 决定呈现过时视图的时间 接着 我可以通过小组件扩展中声明的 ActivityConfiguration 来提供过时的视图 然后 我所要做的就是让视图可以 对 ActivityViewContext 中的 isStale 属性作出反应 在同时出现了多个冒险实时活动时 我想确保这些活动 在锁屏上正确排列 即 较重要的更新接近顶部 最重要的更新出现在灵动岛上 通过提供可选的“relevance-score”字段 我便可以实现这种排列 数字越大意味着相关性越高 现在 你已经了解了 如何使用推送通知进行更新 接下来 你就可以 将其添加到 App 中了 首先 你需要配置服务器和 App 让其支持 ActivityKit 推送通知 然后 测试从终端发送的推送更新 以进行快速迭代 对结果感到满意后 你就可以在服务器上 实现端到端的支持 同时 你应该始终重视用户体验 使用恰当的优先级 并在必要时向用户发出提醒 希望你享受这段 与我共同学习实时活动的时光 很期待看到你为灵动岛和锁屏 带来的所有创新想法
感谢你的观看 ♪ ♪
-
-
3:53 - Enabling push updates
func startActivity(hero: EmojiRanger) throws { let adventure = AdventureAttributes(hero: hero) let initialState = AdventureAttributes.ContentState( currentHealthLevel: hero.healthLevel, eventDescription: "Adventure has begun!" ) let activity = try Activity.request( attributes: adventure, content: .init(state: initialState, staleDate: nil), pushType: .token ) Task { for await pushToken in activity.pushTokenUpdates { let pushTokenString = pushToken.reduce("") { $0 + String(format: "%02x", $1) } Logger().log("New push token: \(pushTokenString)") try await self.sendPushToken(hero: hero, pushTokenString: pushTokenString) } } }
-
6:54 - APNs push payload: Updating
{ "aps": { "timestamp": 1685952000, "event": "update", "content-state": { "currentHealthLevel": 0.941, "eventDescription": "Power Panda found a sword!" } } }
-
7:37 - Printing content state JSON
let contentState = AdventureAttributes.ContentState( currentHealthLevel: 0.941, eventDescription: "Power Panda found a sword!" ) let encoder = JSONEncoder() encoder.outputFormatting = .prettyPrinted let json = try! encoder.encode(contentState) Logger().log("\(String(data: json, encoding: .utf8)!)")
-
9:18 - Terminal: Constructing an APNs request with curl
curl \ --header "apns-topic: com.example.apple-samplecode.Emoji-Rangers.push-type.liveactivity" \ --header "apns-push-type: liveactivity" \ --header "apns-priority: 10" \ --header "authorization: bearer $AUTHENTICATION_TOKEN" \ --data '{ "aps": { "timestamp": '$(date +%s)', "event": "update", "content-state": { "currentHealthLevel": 0.941, "eventDescription": "Power Panda found a sword!" } } }' \ --http2 https://api.sandbox.push.apple.com/3/device/$ACTIVITY_PUSH_TOKEN
-
14:21 - APNs push payload: Alerting
{ "aps": { "timestamp": 1685952000, "event": "update", "content-state": { "currentHealthLevel": 0.0, "eventDescription": "Power Panda has been knocked down!" }, "alert": { "title": "Power Panda is knocked down!", "body": "Use a potion to heal Power Panda!", "sound": "default" } } }
-
14:56 - APNs push payload: Alert localization
{ "aps": { "timestamp": 1685952000, "event": "update", "content-state": { "currentHealthLevel": 0.0, "eventDescription": "Power Panda has been knocked down!" }, "alert": { "title": { "loc-key": "%@ is knocked down!", "loc-args": ["Power Panda"] }, "body": { "loc-key": "Use a potion to heal %@!", "loc-args": ["Power Panda"] }, "sound": "HeroDown.mp4" } } }
-
15:25 - APNs push payload: Alert sound
{ "aps": { "timestamp": 1685952000, "event": "update", "content-state": { "currentHealthLevel": 0.0, "eventDescription": "Power Panda has been knocked down!" }, "alert": { "title": { "loc-key": "%@ is knocked down!", "loc-args": ["Power Panda"] }, "body": { "loc-key": "Use a potion to heal %@!", "loc-args": ["Power Panda"] }, "sound": "HeroDown.mp4" } } }
-
15:52 - APNs push payload: Dismissal
{ "aps": { "timestamp": 1685952000, "event": "end", "dismissal-date": 1685959200, "content-state": { "currentHealthLevel": 0.23, "eventDescription": "Adventure over! Power Panda is taking a nap." } } }
-
16:44 - APNs push payload: Stale date
{ "aps": { "timestamp": 1685952000, "event": "update", "stale-date": 1685959200, "content-state": { "currentHealthLevel": 0.79, "eventDescription": "Egghead is in the woods and lost connection." } } }
-
16:54 - Displaying a stale Live Activity UI
struct AdventureActivityConfiguration: Widget { var body: some WidgetConfiguration { ActivityConfiguration(for: AdventureAttributes.self) { context in AdventureLiveActivityView( hero: context.attributes.hero, isStale: context.isStale, contentState: context.state ) .activityBackgroundTint(Color.gameWidgetBackground) } dynamicIsland: { context in // ... } } }
-
17:19 - APNs push payload: Relevance score
{ "aps": { "timestamp": 1685952000, "event": "update", "relevance-score": 100, "content-state": { "currentHealthLevel": 0.941, "eventDescription": "Power Panda found a sword!" } } }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。