Bring widgets to life
Swift & UI 进阶 20m

让小组件动起来:动画与交互

Bring widgets to life

2023年6月5日

在 Apple 官方观看视频

一句话判断

Widget 终于支持动画和交互了——用 App Intents 驱动按钮和开关操作,用 SwiftUI 的动画 API 让内容切换有感知力,再加上新的 Preview API 实时预览动画效果,这让 Widget 从”静态信息卡”变成了”可以操作的迷你 App”。

这场 Session 讲了什么

Session 聚焦 Widget 的两项重大更新:

动画

  • Widget 使用时间线(timeline)驱动动画,而非传统的状态驱动。系统对比前后两个 entry 的视图差异,自动对变化部分做动画。
  • 默认提供弹簧动画和内容过渡效果。
  • 可以使用 SwiftUI 的所有 transition、animation、contentTransition API 自定义动画。
  • 新增 numericText content 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 之间生效,不能做连续的实时动画(比如旋转加载圈)。
WWDC 2023