为 Widget 构建 SwiftUI 视图
Build SwiftUI views for widgets
2020年6月24日
一句话判断
Widget 的 SwiftUI 视图不是普通 App 视图的缩小版——系统对其有很多限制(不支持滚动、不支持动画、不能用某些 View Modifier),这场 Session 教你如何在这个约束条件下做出好看的 Widget。
这场 Session 讲了什么
Widget 的 UI 完全用 SwiftUI 构建,但不能使用所有 SwiftUI 特性。Session 详细列出了哪些可用、哪些不可用。不可用的包括:滚动视图、导航链接、动画、按钮交互、文本输入等。可用的核心是静态布局:VStack、HStack、ZStack、Image、Text、Shape 以及各种 View Modifier。
Session 重点讲解了 Widget 视图的设计原则。Widget 在不同尺寸下(small、medium、large)应该展示不同层级的信息。small 只展示一个核心数据点,medium 可以展示标题+数据+趋势,large 可以展示多条数据或详细内容。
另一个重要话题是 Widget 的渲染机制。系统会将 Widget 的 SwiftUI 视图渲染为快照(类似截图),显示在主屏幕上。这意味着你的 Widget 视图代码不会持续运行——它只在系统刷新 Widget 时执行一次。
值得深挖的点
Widget 视图的生命周期是”快照式”的。 与 App 内的 SwiftUI 视图不同,Widget 视图不会持续存在。系统在刷新时执行你的视图代码,将结果渲染为位图,然后显示在主屏幕上。直到下次刷新前,视图代码都不会再执行。这解释了为什么动画和交互在 Widget 中不可用。
containerBackground 和 Widget 的视觉一致性。 Widget 需要与系统主屏幕风格保持一致。Session 建议使用系统提供的容器背景,让 Widget 的外观随系统主题变化。自定义背景时要确保在浅色和深色模式下都好看。
代码片段
import SwiftUI
import WidgetKit
// Small Widget——只展示最核心的信息
struct SmallWidgetView: View {
var entry: StepEntry
var body: some View {
VStack(spacing: 4) {
Image(systemName: "flame.fill")
.font(.title2)
.foregroundColor(.orange)
Text("\(entry.steps)")
.font(.system(.title, design: .rounded))
.bold()
Text("步")
.font(.caption2)
.foregroundColor(.secondary)
}
.containerBackground(for: .widget) {
// 系统提供的容器背景
Color.clear
}
}
}
// Medium Widget——标题 + 数据 + 趋势
struct MediumWidgetView: View {
var entry: StepEntry
var body: some View {
HStack {
VStack(alignment: .leading, spacing: 8) {
Text("今日步数")
.font(.caption)
.foregroundColor(.secondary)
Text("\(entry.steps)")
.font(.system(.title, design: .rounded))
.bold()
// 进度条
ProgressView(value: Double(entry.steps), total: 10000)
.tint(entry.steps >= 10000 ? .green : .blue)
}
Spacer()
// 目标完成状态
if entry.steps >= 10000 {
Image(systemName: "checkmark.circle.fill")
.font(.largeTitle)
.foregroundColor(.green)
}
}
.padding()
.containerBackground(for: .widget) {
Color.clear
}
}
}
// Large Widget——多组数据展示
struct LargeWidgetView: View {
var entry: WeeklyEntry
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("本周步数")
.font(.headline)
ForEach(entry.dailySteps.prefix(7)) { day in
HStack {
Text(day.weekday)
.font(.caption)
.frame(width: 30, alignment: .leading)
// 简单的条形图
GeometryReader { geo in
RoundedRectangle(cornerRadius: 2)
.fill(day.steps >= 10000 ? Color.green : Color.blue)
.frame(width: geo.size.width * min(Double(day.steps) / 10000, 1.0))
}
.frame(height: 8)
Text("\(day.steps)")
.font(.caption2)
.foregroundColor(.secondary)
}
}
}
.padding()
.containerBackground(for: .widget) {
Color.clear
}
}
}
最佳实践
- 为每种尺寸单独设计视图。 不要试图用一个视图适配所有尺寸,small 和 large 的信息密度完全不同。
- 使用
containerBackground保持视觉一致性。 不要硬编码背景色,让系统决定 Widget 的容器样式。 - 测试深色模式。 Widget 在深色主屏幕上的可读性与浅色模式下不同,确保文字和图标的对比度足够。
- 控制视图层级深度。 Widget 渲染有时间和内存限制,过于复杂的视图层级可能导致渲染失败。
还有什么值得关注
- Widget 视图中可以使用
Link来创建可点击区域,跳转到 App 的不同页面 IntentConfiguration可以让用户自定义 Widget 内容(如选择追踪的城市)- 系统会在 Widget Gallery 中使用
getSnapshot的数据渲染预览 - Widget 的视图代码应该尽量轻量,避免复杂的计算