Principles of great widgets
Design 入门 22m

优秀小组件的设计原则

Principles of great widgets

2021年6月9日

在 Apple 官方观看视频

一句话判断

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 的不同页面。
WWDC 2021