让小组件动起来:动画与交互
Bring widgets to life
2023年6月5日
一句话判断
Widget 终于支持动画和交互了——用 App Intents 驱动按钮和开关操作,用 SwiftUI 的动画 API 让内容切换有感知力,再加上新的 Preview API 实时预览动画效果,这让 Widget 从”静态信息卡”变成了”可以操作的迷你 App”。
这场 Session 讲了什么
Session 聚焦 Widget 的两项重大更新:
动画:
- Widget 使用时间线(timeline)驱动动画,而非传统的状态驱动。系统对比前后两个 entry 的视图差异,自动对变化部分做动画。
- 默认提供弹簧动画和内容过渡效果。
- 可以使用 SwiftUI 的所有 transition、animation、contentTransition API 自定义动画。
- 新增
numericTextcontent transition,专门为数值变化设计——咖啡因从 200mg 变成 350mg 时数字会”跳动”。 - 新的 Xcode Preview API 可以在编辑器中实时预览 widget 时间线动画,无需等待实际渲染。
交互:
- Widget 中的 Button 和 Toggle 现在可以触发操作。
- 动作通过 App Intents 实现——你定义一个遵循
AppIntent协议的类型,系统在独立的 widget extension 进程中执行它。 - 交互触发的 timeline reload 是保证执行的(不同于普通 reload 的 best-effort)。
- 交互式 widget 也适用于 Live Activities 和 Dynamic Island。
Session 用一个咖啡因追踪 App 贯穿全场——展示数值动画、列表项的推入过渡、以及直接在 widget 上记录新饮品的交互。
值得深挖的点
Widget 的架构模型对理解交互至关重要。你的视图代码只在 timeline 归档时运行,系统渲染的是归档后的视图表示,不是你的代码。这意味着 Button 的 closure 不会在 App 进程中执行——它通过 App Intents 在 widget extension 进程中运行。理解这个模型才能正确设计交互逻辑。
交互触发的 reload 保证执行是个关键的设计决策。普通 widget reload 受系统调度影响,可能被延迟或跳过。但用户在 widget 上点击按钮后,系统保证会触发 reload 来更新显示。这让交互的”操作-反馈”闭环得以成立。
numericText content transition 不仅仅是数字变化动画——它给数字赋予了视觉重要性。在你的 widget 中展示金额、数量、分数这类关键数值时,使用这个 transition 能让用户注意到变化。Apple 把它定位为”给重要数值以突出表现”的工具。
代码片段
Widget 动画自定义:
struct CaffeineTrackerEntryView: View {
let entry: SimpleEntry
var body: some View {
VStack {
// 数值变化动画——数字跳动效果
Text("\(entry.caffeineAmount)mg")
.contentTransition(.numericText())
// 当 caffeineAmount 变化时,数字会以动画方式过渡
// 列表项的推入过渡
if let lastDrink = entry.lastDrink {
Text(lastDrink.name)
.id(lastDrink.id) // 用 id 标识视图身份
.transition(.push(from: .bottom))
.animation(.smooth(duration: 0.3), value: lastDrink.id)
}
}
}
}
Widget 交互(通过 App Intent):
// 定义一个 App Intent 来处理 widget 上的按钮点击
struct LogDrinkIntent: AppIntent {
static var title: LocalizedStringResource = "记录饮品"
static var description = IntentDescription("记录一杯新的饮品")
func perform() async throws -> some IntentResult {
// 执行记录逻辑——更新数据模型
// 系统保证随后会 reload widget timeline
// 所以你不需要手动触发 reload
return .result()
}
}
// 在 widget 视图中使用 Button
struct CaffeineWidget: Widget {
var body: some WidgetConfiguration {
StaticConfiguration(kind: "CaffeineWidget") { entry in
CaffeineEntryView(entry: entry)
.containerBackground(.fill.tertiarySystemBackground, for: .widget)
}
}
}
// widget 视图中的交互按钮
Button(intent: LogDrinkIntent()) {
Text("记录饮品")
}
Xcode Preview 中预览 widget 动画:
// 新的 Preview API:预览整个 timeline 的动画效果
#Preview(as: .systemSmall) {
CaffeineWidget()
} timeline: {
SimpleEntry(date: .now, caffeineAmount: 200, lastDrink: Drink(name: "拿铁"))
SimpleEntry(date: .now + 600, caffeineAmount: 350, lastDrink: Drink(name: "美式"))
SimpleEntry(date: .now + 1200, caffeineAmount: 500, lastDrink: Drink(name: "浓缩"))
}
// 在 canvas 中点击切换 entry,可以看到动画过渡效果
最佳实践
- 数值类的关键指标用
.numericText()content transition,给变化以视觉强调。 - 列表项变化时用
.id()标识身份,配合 transition 实现推入/滑出效果。 - 交互操作通过 App Intent 实现,不要试图在 widget 中维护状态——widget 没有状态。
- 交互后依赖系统自动触发的 reload 更新 UI,不需要手动调用
reloadTimelines。 - 用新的 Preview API 调试动画,比等待实际渲染效率高得多。
- widget 的动画和交互能力同样适用于 Live Activities,可以统一设计语言。
还有什么值得关注
- 交互式 widget 需要 App Intent 支持,如果你已经在 Shortcuts/Siri 中定义了相关 Intent,可以直接复用。
- “Explore SwiftUI Animation” Session 提供了更底层的动画原理解释。
- “Build programmatic UI with Xcode Previews” Session 详细介绍了新的 Preview API。
- Widget 的动画由系统渲染,不受 App 进程生命周期影响——即使 App 被杀死,widget 动画仍然正常。
- 动画只在 timeline entry 之间生效,不能做连续的实时动画(比如旋转加载圈)。