优秀小组件的设计原则
Principles of great widgets
2021年6月9日
一句话判断
Widget 不是迷你 app——这是这场 Session 最核心的观点。如果你的 Widget 试图做太多事情,大概率会既不好用又不好看。
这场 Session 讲了什么
Widget(小组件)在 iOS 14 推出后,Apple 观察了大量第三方 Widget 的设计质量,总结出了这份设计原则指南。这场 Session 不讲技术实现,而是讲设计哲学。
三个核心原则:Glanceable(一目了然)、Personalized(个性化)、Contextual(场景化)。
Glanceable 意味着用户不需要进入 app 就能获取关键信息。好的 Widget 应该在 1-2 秒内传达最有价值的信息。反面教材是在 Widget 上塞满数据,让用户需要仔细阅读才能理解。
Personalized 意味着 Widget 应该根据用户的使用习惯展示不同的内容。天气 Widget 应该显示用户所在城市的天气,而不是一个固定城市的。股票 Widget 应该显示用户关注的股票。
Contextual 意味着 Widget 应该根据时间和位置调整内容。早上显示今日日程,下午显示通勤路况,晚上显示明天的天气。WidgetKit 的 TimelineProvider 支持为不同的时间点提供不同的视图。
值得深挖的点
Widget 的信息密度设计
Apple 建议 Widget 的信息元素不超过 3 个。一个天气 Widget 应该只显示:当前温度、天气图标、城市名。如果你还想加湿度、风速、紫外线指数,那信息量就太多了。更好的做法是把这些额外信息放在用户点击 Widget 后打开的 app 页面中。
这个原则在小型 Widget(systemSmall)上尤其重要。小型 Widget 只有 155x155 点,放不下太多东西。很多开发者试图在小型 Widget 上加可滚动的内容,但 WidgetKit 不支持交互,只能展示静态内容。
深色/浅色模式的适配策略
Widget 需要同时适配浅色和深色模式。Session 建议用 @Environment(\.colorScheme) 检测当前模式,然后调整颜色和图片。但更关键的是:Widget 的背景不应该是纯色的。Apple 推荐使用 ContainerBackground 或者渐变背景,让 Widget 在主屏壁纸上有辨识度。
代码片段
// Widget 的 TimelineProvider 配置
import WidgetKit
struct WeatherTimelineProvider: TimelineProvider {
func placeholder(in context: Context) -> WeatherEntry {
WeatherEntry(date: Date(), temperature: 22, condition: .sunny)
}
func getSnapshot(in context: Context, completion: @escaping (WeatherEntry) -> Void) {
completion(WeatherEntry(date: Date(), temperature: 22, condition: .sunny))
}
func getTimeline(in context: Context, completion: @escaping (Timeline<WeatherEntry>) -> Void) {
// 为未来几个小时生成时间线条目
let now = Date()
var entries: [WeatherEntry] = []
// 每小时一个条目,共 12 个
for hourOffset in 0..<12 {
let date = Calendar.current.date(byAdding: .hour, value: hourOffset, to: now)!
let entry = WeatherEntry(
date: date,
temperature: forecastTemp(for: hourOffset),
condition: forecastCondition(for: hourOffset)
)
entries.append(entry)
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
struct WeatherEntry: TimelineEntry {
let date: Date
let temperature: Int
let condition: WeatherCondition
}
// 简洁的 Widget 视图设计
struct WeatherWidgetEntryView: View {
let entry: WeatherEntry
var body: some View {
VStack(alignment: .leading, spacing: 4) {
// 只显示三个关键信息
Text(entry.date, style: .time)
.font(.caption2)
.foregroundColor(.secondary)
HStack(alignment: .firstTextBaseline) {
Text("\(entry.temperature)")
.font(.system(size: 40, weight: .bold, design: .rounded))
Text("°")
.font(.title2)
}
Label(entry.condition.description, systemImage: entry.condition.icon)
.font(.caption)
.foregroundColor(.secondary)
}
.padding()
}
}
最佳实践
不要在你的 Widget 上放广告或者促销内容。Apple 的审核指南明确禁止 Widget 作为广告位。Widget 应该只展示对用户有价值的信息。
Widget 的刷新频率是系统控制的。TimelineReloadPolicy 有三种:.atEnd(最后一条 entry 的时间到了再刷新)、.after(date)(指定时间刷新)、.never(不自动刷新)。不要设置过于频繁的刷新——系统会根据电量和使用情况延迟刷新。
还有什么值得关注
- iOS 15 支持在 Widget 中使用
IntentConfiguration来做可配置的 Widget(比如选择城市、选择股票)。 WidgetCenter新增了reloadTimelines(ofKind:)方法,可以在 app 中主动触发 Widget 刷新。- 大型 Widget(systemLarge)可以利用
Link组件让不同区域点击后跳转到 app 的不同页面。