初识 WidgetKit
Meet WidgetKit
2020年6月22日
一句话判断
iOS 14 的 Widget 是今年最大的 UI 变革——WidgetKit 用 SwiftUI 声明式 UI + Timeline Provider 的架构,彻底取代了_today extension_时代的老旧方案,每个 iOS 开发者都应该了解。
这场 Session 讲了什么
WidgetKit 是 iOS 14 中全新的框架,用于在主屏幕和”今天”视图中展示 App 的信息卡片。与旧版 Today Extension 的最大区别是:Widget 不是迷你 App,而是纯展示组件——用户不能直接在 Widget 上交互,点击 Widget 会跳转到主 App。
架构上,Widget 由三个核心部分组成:TimelineProvider(提供 Widget 显示的数据和时间线)、Widget(SwiftUI 视图,定义 Widget 的外观)、以及 WidgetBundle(如果 App 有多个 Widget,用 Bundle 组织它们)。
Timeline 是 Widget 的核心概念。你可以提前提供一组带时间戳的数据(Timeline Entry),系统会在对应时间自动刷新 Widget。比如天气 App 可以提供未来 24 小时每小时一条的天气数据,系统会在每个整点自动更新显示,无需 App 在后台运行。
值得深挖的点
Timeline 策略决定了 Widget 的”新鲜度”。 系统并不会严格按你提供的时间线刷新——它有一个全局的刷新预算,会根据用户的使用习惯、电池状态等因素调整。你需要告诉系统下一次刷新的最早时间(nextUpdateDate),但实际刷新可能晚于这个时间。
Widget 的三种尺寸。 系统提供 small、medium、large 三种尺寸。你的 SwiftUI 视图需要通过 @Environment(\.widgetFamily) 来适配不同尺寸。不是所有内容都适合所有尺寸——small 只能显示一两个关键数据,large 才能展示列表。
代码片段
import WidgetKit
import SwiftUI
// 1. 定义 Timeline Entry——每个时间点的数据
struct SimpleEntry: TimelineEntry {
let date: Date // 这条数据应该在什么时间显示
let configuration: ConfigurationIntent
let temperature: Double
let condition: String
}
// 2. Timeline Provider——系统调用这个来获取数据
struct Provider: TimelineProvider {
func placeholder(in context: Context) -> SimpleEntry {
// 占位数据,在 Widget 首次加载或加载中时显示
SimpleEntry(date: Date(), configuration: .init(), temperature: 22, condition: "晴")
}
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> Void) {
// 在 Widget Gallery 中展示的预览数据
completion(SimpleEntry(date: Date(), configuration: .init(), temperature: 22, condition: "晴"))
}
func getTimeline(in context: Context, completion: @escaping (Timeline<SimpleEntry>) -> Void) {
// 提供完整的时间线数据
let entries = (0..<24).map { hour -> SimpleEntry in
let entryDate = Calendar.current.date(byAdding: .hour, value: hour, to: Date())!
return SimpleEntry(
date: entryDate,
configuration: .init(),
temperature: Double.random(in: 15...30),
condition: "多云"
)
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
// 3. Widget 的 SwiftUI 视图
struct WeatherWidgetEntryView: View {
var entry: Provider.Entry
@Environment(\.widgetFamily) var family
var body: some View {
switch family {
case .systemSmall:
VStack {
Text("\(Int(entry.temperature))°")
.font(.largeTitle)
.bold()
Text(entry.condition)
.font(.caption)
.foregroundColor(.secondary)
}
case .systemMedium:
HStack {
VStack(alignment: .leading) {
Text("当前天气")
.font(.caption)
Text("\(Int(entry.temperature))°")
.font(.title)
.bold()
}
Spacer()
Text(entry.condition)
.font(.title3)
}
.padding()
default:
// Large 尺寸可以显示更多内容
VStack(alignment: .leading) {
Text("天气预报")
.font(.headline)
Divider()
Text("\(Int(entry.temperature))° - \(entry.condition)")
}
.padding()
}
}
}
// 4. 组装 Widget
@main
struct WeatherWidgetBundle: WidgetBundle {
var body: some Widget {
WeatherWidget()
}
}
struct WeatherWidget: Widget {
let kind: String = "WeatherWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
WeatherWidgetEntryView(entry: entry)
}
.configurationDisplayName("天气")
.description("显示当前和未来天气")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
}
}
最佳实践
- 提供丰富的 Timeline 数据。 不要只给当前时间的数据,预生成未来几个小时甚至一天的数据,让系统在合适的时机自动刷新。
- 为 Widget Gallery 提供有意义的快照数据。
getSnapshot的数据会直接展示在用户选择 Widget 的画廊中,用真实数据比占位数据更能吸引用户添加。 - small 尺寸只展示最关键的信息。 一两个数字、一个图标加状态文字就够了。不要试图在小 Widget 里塞太多内容。
- 使用
.widgetURL和.link处理点击跳转。 Widget 点击后通过 Deep Link 跳转到 App 的对应页面,让用户快速获取详细信息。
还有什么值得关注
- Widget 支持 Intent Configuration,用户可以在 Widget 上长按配置参数(如选择城市)
- Timeline Policy 有三种:.atEnd、.after(date)、.never,控制下次刷新时机
- 系统限制每个 App 的 Widget 每天刷新次数,不要浪费刷新预算
- Widget 跑在独立的进程中,与主 App 的数据共享需要通过 App Group