大多数浏览器和
Developer App 均支持流媒体播放。
-
了解 ActivityKit
实时活动是用户在 App 中跟踪任务进度的一种简单明了的方式。我们将向你介绍如何为锁屏、灵动岛和待机创建有用的实时活动体验。欢迎了解如何更新 App 的实时活动、监控活动状态以及利用 WidgetKit 和 SwiftUI 构建更丰富的体验。
章节
- 0:00 - Intro
- 0:36 - Live Activity overview
- 4:23 - Lifecycle of Live Activities
- 10:43 - Building Live Activity UI
- 16:37 - Wrap-up
资源
- ActivityKit
- Displaying live data with Live Activities
- Human Interface Guidelines: Live Activities
- Starting and updating Live Activities with ActivityKit push notifications
- WidgetKit
相关视频
WWDC23
-
下载
♪ ♪
Can:大家好 我是 Can Aran 是 iOS System Experience 团队的一名工程师 很高兴向大家介绍实时活动 在本讲座中 我将为你概述什么是实时活动 然后 我将介绍 实时活动的生命周期 最后 我将向你展示如何为你的活动 构建简洁的沉浸式 UI 先来了解一下实时活动的作用 实时活动这种 沉浸式、简单明了的方式 可用于追踪事件或任务进度 实时活动的开始和结束都是离散的 可通过后台 App 运行时进行实时更新 或使用推送通知远程进行更新 以下是来自美国联合航空公司 和 MLB 品牌的优秀示例 在 iPhone 14 Pro 和 Pro Max 上 实时活动会更具沉浸感 当 App 在后台运行时 灵动岛会通过系统 显示实时活动 当一个实时活动处于活动状态时 将显示可变宽度的“紧凑式”呈现 灵动岛最多可同时显示 两个实时活动 其中一个实时活动在 原深感摄像头上显示 另一个则在自己的独立视图中呈现 这两个实时活动都使用“极简式”呈现 用户可随时长按实时活动 显示“扩展式”呈现 从而获得更为简单明了的信息 在扩展式呈现中 视图可深度链接到 App 中的 不同区域 提供了丰富的用户体验 在 iOS 17 中 实时活动有一些新的体验 除了锁屏和灵动岛 待机状态下也可显示实时活动 现在 iPad 也支持实时活动 在 iPadOS 上启用你的实现方案 将你的沉浸式 实时活动加入到 iPad 就比如 Crumbl Cookies 的这个 在 iOS 17 中 你可使用 WidgetKit 和 SwiftUI 为实时活动添加交互 可添加按钮或切换开关 来增强用户体验 请观看 Luca 的讲座 “让小组件生动起来” 进一步了解如何为小组件 注入更多交互能力的信息 实时活动 依赖于 ActivityKit 框架 使你的 App 能够请求、 更新和管理其生命周期 实时活动使用 SwiftUI 和 WidgetKit 进行声明式布局 如果你之前已经实施过主屏幕 小组件 那你对此会感觉非常熟悉 当你的 App 在前台运行时 可请求实时活动 App 只能在离散的用户操作之后 可能是“紧随”某事件或明确开始一项 任务之后 再请求实时活动 这对于确保 积极的用户体验至关重要 实时活动类似于通知 由用户进行管理 用户可轻松地关闭 或完全关掉你的 App API 要求你支持所有锁屏呈现 和全部三种灵动岛的呈现 在待机状态下 系统会按比例 缩放你的锁屏呈现 以填满屏幕 除了依赖后台运行时之外 你的 App 还可使用 “liveactivity”推送类型的 推送通知来远程更新实时活动 有关如何使用推送通知 更新实时活动的更多信息 可观看 Jeff 的讲座 App 的实时活动 在其生命周期中会经历不同阶段 我正在构建一个实时活动 用户可从 Emoji Rangers App 中 选择一个英雄并带他去冒险 在冒险过程中 英雄将面临挑战并与终极对手决斗 我将在我的实时活动中 显示该冒险的重要时刻 这个实时活动显示了英雄冒险的 最基本信息 包括英雄的姓名 和统计数据、英雄的头像、 健康水平以及一个描述 说明该英雄在其冒险中的经历 实时活动的生命周期 包含四个主要步骤 先请求一个活动 活动开始后 使用你的最新内容来更新该活动 同时 观察你的活动 以便对其状态更改做出反应 比如用户结束了该活动 当任务完成时 请务必结束活动 请求实时活动非常直接 请确保你的 App 在前台运行 并对其进行配置 这样你就有了初始内容 和必要的活动请求数据 在 Emoji Rangers App 中 请求实时活动之前 我必须先通过实现 “ActivityAttributes” 来定义一组静态和动态数据 我称之为“AdventureAttributes” “AdventureAttributes” 描述了一个静态数据 即英雄 它还定义了所需的自定义 “ContentState” 其中封装了 英雄的健康水平和事件描述 随着这些属性的变化 我的 实时活动 UI 将得到更新 我将能在屏幕上 显示当前的冒险状态 动态和静态数据现已准备就绪 我将设置冒险活动请求 先开始为英雄创建 AdventureAttributes 实例 并设置初始内容 其中包含英雄的健康水平 和一个事件描述 每个活动内容都可提供失效日期 用于通知系统何时将内容视为过时 现在 我将传入空值 nil 内容的相关性得分决定了 在启动几个冒险活动时 每个实时活动的出现顺序 如果要开始另一个冒险活动 我会为每个活动 指定不同的相关性得分 相关性得分是可选参数 默认值为 0 现在我可以请求活动了 我将传递属性、初始内容 和推送通知类型等参数 推送通知类型会指示实时活动 是否通过 ActivityKit 推送通知 来接收其动态内容的更新 本例中 我将其设置为“nil” 这表示此活动只能在本地接收更新 为了开始此实时活动 需启用 Emoji Rangers App 的 实时活动设置 现在我可以请求我的实时活动了 我会研究如何在我的英雄 经历惊险的任务时更新冒险 动态属性会告知我 何时更新实时活动 每当事件描述 或英雄的健康水平发生变化时 我都会更新我的活动 糟糕!英雄受到了终极对手的重创 于是 我创建了一个反映健康水平变化 和描述事件的“contentState” 由于英雄的健康水平显著降低 我需要发送警报 我将为此创建一个警报配置 如果实时活动的 某些重要信息发生变化 它将在 iPhone、iPad 或同步的 Apple Watch 上显示警报 在本例中 英雄受伤严重 需要药水来治愈 配置的标题和正文 仅在 Apple Watch 上使用 并显示为一个通知 在 iPhone 和 iPad 上 活动 UI 与更新的内容会一起显示 并出现指定的声音 现在可以在活动对象上 调用更新 API 使用更新的内容和警报配置 这能确保实时活动获得更新 并向用户发出此更新的警报 活动状态变化可在实时活动 生命周期中的任意时刻发生 有 4 种可能的状态: “已启动”、“已完成”、 “已关闭”和“已过时” 我使用活动对象上的 activityStateUpdates API 来观察这些状态并以异步接收更新 当活动被关闭时 要确保不再追踪冒险数据 并在 App 中更新 UI 不再显示正在进行的活动 我还可通过 activityState API 来检查状态 以便在需要时对其进行同步检索 我的英雄经历了很多 现在 是时候结束实时活动冒险了 为了能够结束活动 我会先创建一个最终内容 我的内容将显示冒险的最终状态 英雄打败了终极对手 然后 我将确定 UI 的关闭策略 默认策略适用于本例 此策略可确保冒险信息 在结束后会在锁屏上显示一段时间 以便用户可以瞥一眼锁屏 看看在冒险结束时 发生了什么 现在 我可以 结束冒险活动 让英雄休息一下 我已经围绕我的实时活动 生命周期构建了所有逻辑 现在 该专注于活动 UI 了 Emoji Rangers 小组件拓展目前在其 WidgetBundle 中有两个小组件 我需要在 WidgetBundle 中 添加实时活动配置 我称其为 “AdventureActivityConfiguration” “AdventureActivityConfiguration” 利用小组件基础结构 并需要在其 body 内 返回 WidgetConfiguration 我将创建一个 ActivityConfiguration 对象 用于描述实时活动的内容 对于每个呈现闭包 ActivityConfiguration 对象 提供了一个 ActivityViewContext 其中存储了我的静态、 动态属性和活动 ID 此背景信息是基于传入到配置中的 属性类型而创建的 此类型必须与你请求活动时 使用的属性相匹配 我将传递 “AdventureAttributes”类型 以便使活动配置能够成功被初始化 “ActivityConfiguration”中的 第一个闭包 指定了锁屏 UI 我的视图背景信息 随着活动更新而改变 此 UI 将在每次更新时呈现 与小组件类似 我无需为实时活动 提供锁屏 UI 的大小 而是让系统确定适当的尺寸 针对 Emoji Rangers 活动 我将在锁屏上显示英雄信息、 名称和头像、 健康水平以及事件描述 背景为海军蓝 “AdventureLiveActivityView” 将通过传入的视图背景信息 拥有所有这些信息 我的锁屏上的实时活动 看起来简洁、优雅 且提供了我需要的 英雄在冒险中的经历的所有信息 现在 我封装了锁屏 UI 需要实现我的灵动岛呈现 有三种呈现方式: 紧凑式、极简式和扩展式呈现 若 App 的实时活动 是系统上唯一运行的活动 将使用紧凑呈现 紧凑呈现有两个区域:前端和后端 二者会一起出现 形成灵动岛中的一个完整呈现 选择要在前端和后端空间中 显示的关键内容 因为空间有限 用户应能通过查看此处的内容 来识别特定的活动 在 ActivityConfiguration 对象的“DynamicIsland”闭包中 我可再次访问视图背景信息 来创建我的 expanded compactLeading、compactTrailing 和 minimal 视图 我需要创建一个 DynamicIsland 视图构建器 来表示每种呈现 对于我的英雄冒险 我将在前端内容中添加英雄头像 并在后端视图中添加健康水平 我还将有一个动态色调的颜色 其是根据我的英雄健康水平来确定 现在 冒险的紧凑式呈现已准备就绪 有多个 App 启动实时活动时 系统会选择哪些实时活动可见 并使用极简式呈现 来同时显示这两个实时活动: 一个极简式呈现在灵动岛上 而另一个则疏离显示 你的极简式视图应只有最关键的信息 因为你的工作空间非常有限 对于我的实时活动中的极简式视图 要显示的最重要的信息就是 英雄名字以及英雄的健康状况 因此 我将使用动态色调颜色 来显示英雄头像和健康水平 这样 用户就能知道 何时该查看极简视图来帮助英雄 当用户在紧凑或极简呈现中 长按某个实时活动时 系统会扩展呈现其内容 我也需要支持扩展式呈现 对于扩展式呈现 系统将扩展呈现分成了不同区域 DynamicIsland 视图构建器的第一个闭包 表示拓展内容 在该闭包中 可在拓展区域传递指定位置参数 来定义各部分的内容 我将英雄名称 和头像添加到前端空间 英雄统计数据添加到后端空间 最后将健康条 和事件描述添加到底部空间 最后 我的灵动岛 UI 会看起来很简洁 且提供了所有必要的冒险信息 现在 我已准备好用我刚刚创建的 简洁、沉浸式的实时活动 UI 来与我最喜欢的英雄一起冒险 在设计你自己的 UI 时 应在实时活动中 仅显示最基本的内容 要让其保持简洁 并在用户点击实时活动时 在你的 App 上显示更多详细信息 观看讲座“设计动态实时活动” 可获取更多信息 实时活动是一个强大的工具 可使用它来简单明了地 展示正在进行的活动的实时信息 通过其简单的配置 在 iOS 和 iPadOS 上 创建一种动态的、与用户互动的方式 要了解推送更新的更多相关信息 请观看讲座“使用推送通知 来更新实时活动” 我很期待能看到 你使用 ActivityKit 构建的作品 感谢你的观看! ♪ ♪
-
-
5:40 - Define ActivityAttributes
import ActivityKit struct AdventureAttributes: ActivityAttributes { let hero: EmojiRanger struct ContentState: Codable & Hashable { let currentHealthLevel: Double let eventDescription: String } }
-
6:28 - Request Live Activity with initial content state
let adventure = AdventureAttributes(hero: hero) let initialState = AdventureAttributes.ContentState( currentHealthLevel: hero.healthLevel, eventDescription: "Adventure has begun!" ) let content = ActivityContent(state: initialState, staleDate: nil, relevanceScore: 0.0) let activity = try Activity.request( attributes: adventure, content: content, pushType: nil )
-
8:00 - Update Live Activity with new content
let heroName = activity.attributes.hero.name let contentState = AdventureAttributes.ContentState( currentHealthLevel: hero.healthLevel, eventDescription: "\(heroName) has taken a critical hit!" ) var alertConfig = AlertConfiguration( title: "\(heroName) has taken a critical hit!", body: "Open the app and use a potion to heal \(heroName)", sound: .default ) activity.update( ActivityContent<AdventureAttributes.ContentState>( state: contentState, staleDate: nil ), alertConfiguration: alertConfig )
-
9:30 - Observe activity state
// Observe activity state asynchronously func observeActivity(activity: Activity<AdventureAttributes>) { Task { for await activityState in activity.activityStateUpdates { if activityState == .dismissed { self.cleanUpDismissedActivity() } } } } // Observe activity state synchronously let activityState = activity.activityState if activityState == .dismissed { self.cleanUpDismissedActivity() }
-
10:03 - Dismiss Live Activity with final content state
let hero = activity.attributes.hero let finalContent = AdventureAttributes.ContentState( currentHealthLevel: hero.healthLevel, eventDescription: "Adventure over! \(hero.name) has defeated the boss! Congrats!" ) let dismissalPolicy: ActivityUIDismissalPolicy = .default activity.end( ActivityContent(state: finalContent, staleDate: nil), dismissalPolicy: dismissalPolicy) }
-
10:50 - Add ActivityConfiguration to WidgetBundle
import WidgetKit import SwiftUI @main struct EmojiRangersWidgetBundle: WidgetBundle { var body: some Widget { EmojiRangerWidget() LeaderboardWidget() AdventureActivityConfiguration() } }
-
11:05 - Define Lock Screen presentation
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.navyBlue) } dynamicIsland: { context in // ... } } }
-
13:28 - Define Dynamic Island compact presentation
struct AdventureActivityConfiguration: Widget { var body: some WidgetConfiguration { ActivityConfiguration(for: AdventureAttributes.self) { context in // ... } dynamicIsland: { context in DynamicIsland { // ... } compactLeading: { Avatar(hero: context.attributes.hero) } compactTrailing: { ProgressView(value: context.state.currentHealthLevel) { Text("\(Int(context.state.currentHealthLevel * 100))") } .progressViewStyle(.circular) .tint(context.state.currentHealthLevel <= 0.2 ? Color.red : Color.green) } minimal: { // ... } } } }
-
14:42 - Define Dynamic Island minimal presentation
struct AdventureActivityConfiguration: Widget { var body: some WidgetConfiguration { ActivityConfiguration(for: AdventureAttributes.self) { context in // ... } dynamicIsland: { context in DynamicIsland { // ... } compactLeading: { // ... } compactTrailing: { // ... } minimal: { ProgressView(value: context.state.currentHealthLevel) { Avatar(hero: context.attributes.hero) } .progressViewStyle(.circular) .tint(context.state.currentHealthLevel <= 0.2 ? Color.red : Color.green) } } } }
-
15:26 - Define Dynamic Island expanded presentation
struct AdventureActivityConfiguration: Widget { var body: some WidgetConfiguration { ActivityConfiguration(for: AdventureAttributes.self) { context in // ... } dynamicIsland: { context in DynamicIsland { // Leading region DynamicIslandExpandedRegion(.leading) { LiveActivityAvatarView(hero: hero) } // Expanded region DynamicIslandExpandedRegion(.trailing) { StatsView(hero: hero, isStale: isStale) } // Bottom region DynamicIslandExpandedRegion(.bottom) { HealthBar(currentHealthLevel: contentState.currentHealthLevel) EventDescriptionView(hero: hero, contentState: contentState) } } compactLeading: { // ... } compactTrailing: { // ... } minimal: { // ... } } } }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。