Design great widgets
Design 入门 26m

设计出色的桌面小组件

Design great widgets

2020年6月25日

在 Apple 官方观看视频

一句话判断

Widget 不是迷你版 App,而是一种全新的信息呈现方式。Apple 在这个 Session 中详细讲解了 Widget 的设计原则:内容优先、多种尺寸适配、信息密度可控,是做 Widget 设计前的必读指南。

这场 Session 讲了什么

iOS 14 引入了全新的桌面小组件系统(WidgetKit),取代了之前的 Today Extension。Session 从设计角度出发,讲解了 Widget 应该呈现什么内容、如何适配不同尺寸、以及如何设计有效的交互。

Apple 定义了 Widget 的三个设计原则。第一,Glanceable(一览性):用户应该在一秒钟内获取 Widget 的核心信息。第二,Measurable(可衡量性):Widget 应该展示有意义的数据,而不是装饰性的内容。第三,Relevant(相关性):Widget 的内容应该在正确的时间展示正确的信息。Session 还详细介绍了三种尺寸(Small、Medium、Large)的设计策略,以及如何在 Widget 中使用颜色、排版和动效来传达信息。

值得深挖的点

信息架构的优先级设计

Widget 的三种尺寸对应不同的信息展示策略。Small 尺寸只能显示一个核心信息,比如当前的温度。Medium 尺寸可以显示主要信息加一个辅助信息,比如温度加上未来三小时的天气趋势。Large 尺寸可以显示更多信息层级,但仍然要保持核心信息的一览性。Apple 强调:不要试图在 Widget 中复制 App 的完整界面,而是要提炼出最有价值的信息。

时间线驱动的内容更新

Widget 的内容更新不是实时的,而是通过时间线(Timeline)预加载的。你提供一个 Timeline,包含一系列按时间排序的 Entry,系统会在对应的时间点自动切换 Widget 的显示内容。这意味着你需要预测用户在不同时间想看到的信息——比如天气 App 在早上显示今天的天气,下午显示晚上的天气,晚上显示明天的天气。

代码片段

设计 Small 尺寸的 Widget

场景:为任务管理 App 创建一个显示今日待办数量的 Small Widget。

import WidgetKit
import SwiftUI

struct TaskWidgetEntry: TimelineEntry {
    let date: Date
    let taskCount: Int
    let nextTask: String?
}

struct TaskWidgetSmallView: View {
    let entry: TaskWidgetEntry
    
    var body: some View {
        VStack(alignment: .leading, spacing: 4) {
            // 核心信息:待办数量
            Text("\(entry.taskCount)")
                .font(.system(size: 36, weight: .bold, design: .rounded))
                + Text(" 个待办")
                .font(.caption)
                .foregroundColor(.secondary)
            
            // 辅助信息:下一个任务
            if let nextTask = entry.nextTask {
                Text(nextTask)
                    .font(.caption)
                    .foregroundColor(.secondary)
                    .lineLimit(1)
            }
        }
        .padding()
        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
        .background(Color(.systemBackground))
    }
}

设计 Medium 尺寸的 Widget

场景:显示天气概览和未来三小时趋势。

struct WeatherWidgetMediumView: View {
    let entry: WeatherEntry
    
    var body: some View {
        HStack {
            // 左侧:当前天气(核心信息)
            VStack(alignment: .leading, spacing: 2) {
                Text(entry.city)
                    .font(.caption)
                    .foregroundColor(.secondary)
                
                HStack(alignment: .firstTextBaseline) {
                    Text("\(entry.temperature)°")
                        .font(.system(size: 40, weight: .medium))
                    
                    Text(entry.condition)
                        .font(.title3)
                        .foregroundColor(.secondary)
                }
                
                Text("最高 \(entry.high)° 最低 \(entry.low)°")
                    .font(.caption2)
                    .foregroundColor(.secondary)
            }
            
            Spacer()
            
            // 右侧:未来三小时趋势
            VStack(alignment: .trailing, spacing: 4) {
                ForEach(entry.hourlyForecast.prefix(3)) { hour in
                    HStack(spacing: 4) {
                        Text(hour.time)
                            .font(.caption2)
                        Text("\(hour.temperature)°")
                            .font(.caption2)
                            .fontWeight(.medium)
                    }
                }
            }
        }
        .padding()
    }
}

创建智能时间线

场景:根据时间动态调整 Widget 内容。

struct WeatherTimelineProvider: TimelineProvider {
    
    func getTimeline(in context: Context,
                     completion: @escaping (Timeline<WeatherEntry>) -> Void) {
        // 获取天气预报数据
        WeatherService.fetchForecast { forecast in
            var entries: [WeatherEntry] = []
            
            let now = Date()
            let calendar = Calendar.current
            let hour = calendar.component(.hour, from: now)
            
            // 为接下来的 12 小时创建条目
            for hourOffset in 0..<12 {
                let entryDate = calendar.date(byAdding: .hour,
                                               value: hourOffset,
                                               to: now)!
                
                let hourlyData = forecast.hourly[hourOffset]
                let entry = WeatherEntry(
                    date: entryDate,
                    city: forecast.city,
                    temperature: hourlyData.temperature,
                    condition: hourlyData.condition,
                    high: forecast.todayHigh,
                    low: forecast.todayLow,
                    hourlyForecast: Array(forecast.hourly[hourOffset..<min(hourOffset+3, forecast.hourly.count)])
                )
                entries.append(entry)
            }
            
            // 下一次更新时间:12 小时后
            let nextUpdate = calendar.date(byAdding: .hour, value: 12, to: now)!
            let timeline = Timeline(entries: entries, policy: .after(nextUpdate))
            completion(timeline)
        }
    }
}

使用 Widget 配置让用户自定义内容

场景:支持用户选择 Widget 显示的城市。

// 在 Widget Configuration 中添加自定义选项
@main
struct WeatherWidgets: WidgetBundle {
    @WidgetBundleBuilder
    var body: some Widget {
        WeatherWidget()           // 单城市 Widget
        WeatherMultiCityWidget()  // 多城市 Widget
    }
}

struct WeatherWidget: Widget {
    let kind: String = "WeatherWidget"
    
    var body: some WidgetConfiguration {
        // 支持用户配置的 Widget
        IntentConfiguration(
            kind: kind,
            intent: CitySelectionIntent.self,  // 自定义 Intent
            provider: WeatherTimelineProvider()
        ) { entry in
            WeatherWidgetEntryView(entry: entry)
                .containerBackground(.fill, for: .widget)
        }
        .configurationDisplayName("天气")
        .description("查看当前天气和预报")
        .supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
    }
}

最佳实践

已有项目:如果你的 App 有 Today Extension,迁移到 WidgetKit。Today Extension 在 iOS 14 中仍然可用但不推荐。WidgetKit 的性能更好(后台预渲染),用户体验更一致。先做 Small 尺寸,再扩展到 Medium 和 Large。

新项目:从产品设计阶段就规划 Widget。思考”用户最想一眼看到什么信息”,然后围绕这个核心信息设计 Widget。提供时间线时要考虑用户的使用节奏——早上、中午、晚上分别显示什么信息最有价值。

还有什么值得关注

  • Widget 支持暗色模式,使用 @Environment(\.colorScheme) 来适配。
  • iOS 14 还引入了 Smart Stack(智能叠放),系统会根据用户习惯自动轮换不同 App 的 Widget。
  • Widget 的刷新频率由系统控制,你只能建议刷新时间,实际刷新可能被延迟以节省电量。
WWDC 2020