大多数浏览器和
Developer App 均支持流媒体播放。
-
如何让小组件更加生动
了解如何为你的 App 和游戏制作兼具动画和交互性的小组件。我们将向你展示如何调整条目过渡的动画,以及使用 SwiftUI 中的 Button 和 Toggle 来增加交互性,以便你可以直接在主屏幕和锁定屏幕上创造强大时刻。
章节
- 1:23 - Animations
- 7:45 - Interactivity
资源
相关视频
WWDC23
WWDC22
-
下载
♪ ♪
Luca:大家好! 我是 Luca 一名 SwiftUI 团队的工程师 今天 我们将向你介绍 如何使用令人兴奋的 新功能让你的小部件更加生动 小组件是 iOS 和 macOS 体验中 最受欢迎的部分 并且随着交互性和动画的加入 这些小组件甚至展现出 前所未有的强大能力
交互性可以让你的用户在小组件中 直接处理数据 从而为执行 App 中的重要操作 打造强大的交互 动画则通过帮助用户感知 内容的变化方式及操作后的结果 为小组件带来了生动活泼的体验 我对这些新功能都十分兴奋 所以我们开始今天的讲座吧 首先 我们会介绍动画 以及为小组件打造精美外观 到底有多容易 接着 我会向你演示如何 在小组件中添加交互性 首先 我们从动画开始 在本次讲座中 我们会使用 我的朋友 Nils 开发的 App 来跟踪一天之内的咖啡因摄入 该 App 现已具备小组件 它可显示咖啡因摄入总量 以及我今天喝的最后一杯饮料 如果我使用最新的 SDK 来重新编译该小组件 那么小组件中的内容只要发生变化 系统就会使用默认动画 来过渡不同的条目 接下来我们会对该小组件进行调整 使其看起来更棒 但在进入 Xcode 前 我先来简要介绍一下 如何将动画与小组件进行结合 在常规 SwiftUI App 中 你会使用状态来驱动视图中的变化 并且 动画是通过使用类似 withAnimation 的修饰符 由状态变化驱动 但是 小组件的工作方式略有不同 小组件没有状态 相反 小组件会创建一个 包含条目的时间线 并与在各个具体时间上 渲染的视图一一对应 SwiftUI 会决定条目间的异同 并对发生变化的部分使用动画 默认情况下 小组件会使用 隐式弹簧动画 以及各种隐式内容过渡 但你也可以使用 SwiftUI 提供的 开箱即用式过渡、动画 以及内容过渡 API 来自定义小组件的动画效果 我不打算再进一步展开讲解 SwiftUI 工作中所有动画图元的细节 如果你想了解这部分内容 请观看 “探索 SwiftUI 动画”精彩讲座 现在 我来打开 Xcode 向你展示如何仅凭一些调整 便可让你的小组件和 清晨的卡布奇诺一样精致 以及新的 Xcode Preview API 是如何帮助你快速迭代这些动画的
这里展示的是小组件所有的视图 主视图使用了 VStack 其中包含两个视图 第一个展示咖啡因总量 第二个展示我今天喝的最后一种饮料 如果有的话 请注意观察我是如何使用 containerBackground 修饰符 来定义小组件背景的 从而该 App 可以显示 在 Mac 和 iPad 新的支持位置 通常情况下 为了查看小组件动画 你需要设置一批条目 并等待其显示在屏幕上的时刻 但这样的话可能会非常繁琐 而且还会拖慢你的速度 不过好在 今年我们引入了 一个很好的解决方案 即新的 Preview API 我可以为 systemSmall 中的 小组件定义一个 Preview 并传入定义小组件的类型 接着 使用此前定义的条目 指定渲染时间轴的方式 当我在画布上完成上述操作后 我就可以看到时间轴的预览 以及每个条目的视觉效果 但快看看这个 如果我逐个点击预览 我便可以看到小组件在 条目间过渡时的动画效果 这真的太棒了! 但这还只是 新的 Preview API 功能的冰山一角 想要进一步了解更多有关 新的 Preview API 功能的信息 请一定要看看这个讲座 “使用 Xcode Previews 构建程序化 UI” 接下来 我们来调整这些动画 首先 我从咖啡因总量的文本开始 当前该文本只是与下一个数值 交叉淡入淡出 但我很想为数值的出现 添加一些亮点 在本例中 视图保持不变 只有文本内容会发生变化 为了对其增添动画效果 我们可以使用内容过渡 我会添加一个 显示咖啡因数值数字文本 这是一个专门为突出 重要数值变化而设计的内容过渡 看起来不错! 现在 我再来专注看看显示 最后一杯饮料的视图 在这里 我想添加一个过渡 来强调新的饮料即将出现 首先 我会使用 ID 修饰符 将该视图的识别符与正在渲染的 特定 log 进行关联 这就会告知 SwiftUI 只要该 log 发生更改 当前就是一个新视图 并且我们需要过渡到新视图 现在我来指定一个过渡效果 推出效果就很不错 该从哪边开始呢? 底部应该可以 现在 你就知道接下来要做什么了 回到预览画布
我喜欢这个从底部进行的过渡 我们来做最后一个调整 在喝了许多咖啡之后 我有些不安 所以我希望这个过渡可以用 动画曲线体现出来 比较好的一点在于 我可以就像在 常规 SwiftUI App 里一样 使用 animation 修饰符 选择间隔较短的平滑弹簧动画 然后将 animation 绑定到 log 中 现在 animation 就会 和咖啡因量一致 我对当前的效果很满意 现在我们来重点看看交互性 借助交互性 你可以直接 在小组件上执行操作 在我们进入 Xcode 之前 我想花几分钟来 谈谈小组件的工作架构 以便你更好地理解交互性工作原理 在创建小组件的时候 你需要定义一个小组件扩展 该扩展会被系统发现 并作为独立进程运行 小组件会定义一个时间线提供程序 来返回一系列条目 也就是小组件模型 如果小组件可见 系统就会启动小组件扩展进程 并向时间线提供程序请求条目 这些条目就会反馈给视图构建器 也就是小组件配置的一部分 然后其便可根据这些条目 生成一系列视图 之后 系统就会生成这些视图的表示 并将其存档在磁盘上 如果需要显示特定条目 系统就会进行解码 并在其进程中 渲染小组件的存档代表 在这里我们先停一下 强调一下最后一点 你的视图代码只会在存档期间运行 系统进程会渲染视图中单独的表示 但如果你的数据不是静态的 那么你可能就需要更新这些条目 无论你什么时候需要更新 小组件上显示的数据 在 App 中调用 reloadTimelines 函数即可实现 这个过程和我刚描述过的完全相同 即重新生成新条目 然后在磁盘上存档视图的新副本 在该架构中 你需要注意以下 3 个要点 首先 在小组件可见时 你的代码不会运行 你可以通过更新时间线条目 来驱动小组件内容的变化 并且这也适用于交互性小组件 通常情况下 小组件的更新 都会尽可能进行 但需要注意的是 通过交互发起的重新加载 总是能确保进行 在介绍完这些内容后 我们一起来看看如何增加交互性 比较好的一点在于 你可以适用熟悉的控件 例如 Button 和 Toggle 来让部分小组件具有交互性 但请记住 由于小组件在 不同的进程中渲染 所以 SwiftUI 不会在进程空间中 执行闭包或更改你的绑定 因此我们需要一个表示行为的方法 可以被小组件扩展执行 以及被系统调用 不过好在我们已经有了解决方案 App Intents 你可能已经用过 App Intents 来将 App 的操作公开给快捷指令或 Siri 而现在 你可以使用同一个 App Intents 来表示小组件中的操作 从本质上来看 App Intents 是一个协议 可让你在代码中 定义由系统执行的操作 例如 我在这里定义了一个 AppIntent 用来在 Todo App 中 切换代办事项 AppIntent 定义了 一些参数作为输入 及一个 perform 异步函数 用于编写 运行 intent 的业务逻辑 App Intents 功能十分强大 想要进一步了解有关该框架的信息 请一定要看看 WWDC22 和 23 中的 “深入了解 App Intents” 以及“探索 App Intents 的 增强功能”讲座 为了支持直接从 UI 中 执行 App Intents 的能力 在你导入 SwiftUI 和 App Intents 时 Button 和 Toggle 上 有一组新的初始化程序 可将 AppIntent 作为参数 并在这些控件交互时执行 intent 你需要注意的是 交互式小组件 只支持使用 AppIntent 的 Button 和 Toggle 其他控件都不会起作用 当然了 这些初始化程序 也适用于 App 这点很棒 因为这样你就可以 在小组件和 App 之间 共享 AppIntent 逻辑 接下来我们回到 Xcode 和 咖啡跟踪 App 并为其增加一些交互性 当前 用户只能通过打开 App 记录饮料 而这里是小组件能发挥作用的地方 可以作为加速器 来展示 App 中最重要的操作 对于我的 App 而言 该操作便是记录新饮料 所以 我们来将其添加到 我已经创建好的文件中 首先 我会定义一个遵循 AppIntent 的类型 来记录新的饮料 我们来为其设置一个可读标题 以供系统适用 然后在存储中记录 espresso 实现 perform 的要求 并返回一个空的意图 result 我想提醒你的是 perform 是一个异步函数 所以如果你需要执行任何异步操作 例如写入数据库 就应该充分利用函数 就像我在这里等待 log 写入操作时所做的那样 一旦 perform 返回 系统就会立即启动重新加载 小组件时间轴 来更新小组件的内容 所以你需要确保在 perform 返回前持久化所有必要信息 再来重新加载更新过的小组件 我已经将饮料硬编码为 espresso 但我还想将特定饮料 传递给 log 为此 我们可以添加带有 @Parameter 属性包装器的存储属性 以及可填充参数的初始化程序 使用该属性包装器十分重要 因为只有标记有 该属性包装器的存储属性 才能得到保存 以及当 intent 在小组件扩展中 执行的时候才可以使用 在我们添加 Button 调用 intent 前 我想强调一下使用 App Intents 在生态系统方面的重要优势 我刚刚定义的 AppIntent 将会用于快捷指令和 Siri 所以在此处定义的投入 将会为你的用户体验带来回报 而远远超过了小组件本身 现在 我们开始为小组件添加按钮 我们需要创建一个 包含 Button 的新视图 在该视图中 我会使用带有 App intent 的 Button 初始化程序 所以我们可以传入刚刚定义的内容 接着 我们使用 Spacer 将该视图添加到小组件其余部分 现在 一切准备就绪 我们来编译和运行一下 看看在小组件上的效果如何 在这里我有一个小建议 实际上你可以直接构建小组件目标 然后 Xcode 就会 帮你将小组件安装到主屏幕上 现在 我的小组件就有了 刚刚定义的按钮 如果轻点该按钮 我就可以记录 最后这一杯 espresso 但我还想额外再做一个更改 来让小组件提供最佳的用户体验 AppIntent 执行结束后 会导致小组件重新加载时间线 这就会带来从操作到 UI 中结果更改的一小段延迟 但是在 Mac 上的 iPhone 小组件中 该延迟会更加明显 所以我们为你提供了一个 开箱即用式的解决方案 例如 在我的小组件中 展示咖啡因总量的数值 在更新后的条目到达前都不会更新 我们可以使用 invalidatableContent 修饰符来注释该视图 我在 iPhone 和 Mac 上 都添加了该小组件 轻点按钮 显示咖啡因总量的视图 展示了一个系统效果 告诉我们在更新之间其值都是无效 刚刚我们看到了 Button 的效果 以及使用 invalidatableContent 修饰符 是如何让你帮助用户改善延迟感的 但你在使用该修饰符时需要明智点 你不需要对所有可能改变的 单个视图进行注释 只对有意义的视图使用该修饰符 从而为你的用户设立恰当的期望 Toggle 则更进一步 可在得到交互时自动更新显示 无需等待与小组件扩展之间的往返 通过预渲染两种配置下的切换样式 这便可以在存档时自动由系统完成 如果你需要定义自己的切换样式 请确保在样式中检查 configuration.isOn 属性 并利用其切换外观 以上就是对交互性和动画的概述 借助动画和交互性 你可以为小组件注入新的活力 并且现在你还可以将其 放置在所有新位置上 无论你的用户身处何处 你都可以为他们带来 这些微小又令人愉悦的交互 所以 请利用新的 Xcode Preview API 来为你的小组件调整动画 寻找 App 中最重要的操作 并将其展示在小部件中 随时随地为你的用户 提供强大的交互 谢谢大家! ♪ ♪
-
-
3:54 - Usage for the container background modifier
.containerBackground(for: .widget) { Color.cosmicLatte }
-
4:22 - Define a preview for the caffeine tracker widget
#Preview(as: WidgetFamily.systemSmall) { CaffeineTrackerWidget() } timeline: { CaffeineLogEntry.log1 CaffeineLogEntry.log2 CaffeineLogEntry.log3 CaffeineLogEntry.log4 }
-
5:41 - Add a numeric text content transition
struct TotalCaffeineView: View { let totalCaffeine: Measurement<UnitMass> var body: some View { VStack(alignment: .leading) { Text("Total Caffeine") .font(.caption) Text(totalCaffeine.formatted()) .font(.title) .minimumScaleFactor(0.8) .contentTransition(.numericText(value: totalCaffeine.value)) } .foregroundColor(.espresso) .bold() .frame(maxWidth: .infinity, alignment: .leading) } }
-
6:21 - Set up transition on LastDrinkView
struct LastDrinkView: View { let log: CaffeineLog var body: some View { VStack(alignment: .leading) { Text(log.drink.name) .bold() Text("\(log.date, format: Self.dateFormatStyle) · \(caffeineAmount)") } .font(.caption) .id(log) .transition(.push(from: .bottom)) } var caffeineAmount: String { log.drink.caffeine.formatted() } static var dateFormatStyle = Date.FormatStyle( date: .omitted, time: .shortened) }
-
7:18 - Configuring animation for the transition
struct LastDrinkView: View { let log: CaffeineLog var body: some View { VStack(alignment: .leading) { Text(log.drink.name) .bold() Text("\(log.date, format: Self.dateFormatStyle) · \(caffeineAmount)") } .font(.caption) .id(log) .transition(.push(from: .bottom)) .animation(.smooth(duration: 1.8), value: log) } var caffeineAmount: String { log.drink.caffeine.formatted() } static var dateFormatStyle = Date.FormatStyle( date: .omitted, time: .shortened) }
-
9:18 - Reload the timeline for a widget
WidgetCenter.shared.reloadTimelines(ofKind: "LocationForecast")
-
13:06 - App intent to log a caffeine drink
import AppIntents struct LogDrinkIntent: AppIntent { static var title: LocalizedStringResource = "Log a drink" static var description = IntentDescription("Log a drink and its caffeine amount.") @Parameter(title: "Drink", optionsProvider: DrinksOptionsProvider()) var drink: Drink init() {} init(drink: Drink) { self.drink = drink } func perform() async throws -> some IntentResult { await DrinksLogStore.shared.log(drink: drink) return .result() } }
-
15:10 - Create view to log a new drink
struct LogDrinkView: View { var body: some View { Button(intent: LogDrinkIntent(drink: .espresso)) { Label("Espresso", systemImage: "plus") .font(.caption) } .tint(.espresso) } }
-
16:28 - Use the invalidatable content modifier
struct TotalCaffeineView: View { let totalCaffeine: Measurement<UnitMass> var body: some View { VStack(alignment: .leading) { Text("Total Caffeine") .font(.caption) Text(totalCaffeine.formatted()) .font(.title) .minimumScaleFactor(0.8) .contentTransition(.numericText(value: totalCaffeine.value)) .invalidatableContent() } .foregroundColor(.espresso) .bold() .frame(maxWidth: .infinity, alignment: .leading) } }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。