为小组件添加配置与智能功能
Add configuration and intelligence to your widgets
2020年6月26日
一句话判断
Widget 不只是静态信息卡片——加上 IntentConfiguration 和 TimelineProvider 的智能调度,你的 Widget 可以变成”最懂用户”的桌面小组件。
这场 Session 讲了什么
这场 Session 是对 iOS 14 Widget(WidgetKit)体系的进阶讲解,聚焦在两个核心能力:配置(Configuration)和智能调度(Intelligence)。配置让用户可以在 Widget 上自定义显示内容——比如选择要展示的城市天气、选择要跟踪的股票。智能调度让系统在最佳时机刷新 Widget 内容——比如在用户通常查看天气的早晨自动更新天气 Widget。
配置基于 IntentConfiguration,复用了 SiriKit 的 Intent 系统。你定义一个 Custom Intent,系统会自动生成一个配置界面。用户在长按 Widget 时可以进入配置模式,选择参数后 Widget 立即更新。这种设计不需要你自己写配置 UI——系统帮你搞定。
智能调度通过 TimelineProvider 的 getTimeline() 方法实现。你向系统提供一个 TimelineEntry 序列,每个 entry 带有一个日期。系统会在你指定的日期自动刷新 Widget。Session 强调了”智能”的含义:你可以根据用户的使用习惯来安排 entry 的时间点——如果用户通常在早上 8 点查看天气,就把最准确的天气 entry 放在 8 点。
值得深挖的点
Timeline 的策略设计
Timeline 不是”每分钟刷新一次”这么简单。系统有预算限制——你的 Widget 每天的刷新次数是有限的。Session 建议采用”重要时刻密集,其他时刻稀疏”的策略。比如股市 Widget:交易时间内每分钟一个 entry,闭市后每小时间隔越来越大(因为价格不再变化)。这样你把有限的刷新预算用在了最需要精确数据的时段。
Reload vs Timeline 的区别
reloadTimelines(ofKind:) 会触发系统重新调用 getTimeline(),生成全新的 entry 序列。这适用于数据发生了重大变化(如用户切换了城市)。reloadAllTimelines() 刷新所有 Widget。Session 提醒:不要频繁调用 reload——每次调用都消耗系统预算。最好通过 getTimeline() 中的 future entries 来预判数据变化,减少 reload 的需求。
代码片段
import WidgetKit
import SwiftUI
import Intents
// 可配置的天气 Widget
struct WeatherWidget: Widget {
let kind: String = "WeatherWidget"
var body: some WidgetConfiguration {
IntentConfiguration(
kind: kind,
intent: CitySelectionIntent.self, // 自定义 Intent
provider: WeatherTimelineProvider()
) { entry in
WeatherWidgetEntryView(entry: entry)
}
.configurationDisplayName("天气")
.description("显示指定城市的天气信息")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
}
}
// Timeline Provider:提供智能调度
struct WeatherTimelineProvider: IntentTimelineProvider {
typealias Entry = WeatherEntry
typealias Intent = CitySelectionIntent
func placeholder(in context: Context) -> WeatherEntry {
WeatherEntry(
date: Date(),
city: "北京",
temperature: 25,
condition: .sunny
)
}
func getSnapshot(for configuration: CitySelectionIntent,
in context: Context,
completion: @escaping (WeatherEntry) -> Void) {
// Widget Gallery 中展示的预览
let city = configuration.city ?? "北京"
completion(WeatherEntry(
date: Date(), city: city, temperature: 25, condition: .sunny
))
}
func getTimeline(for configuration: CitySelectionIntent,
in context: Context,
completion: @escaping (Timeline<WeatherEntry>) -> Void) {
let city = configuration.city ?? "北京"
let now = Date()
// 生成未来 12 小时的 entry 序列
var entries: [WeatherEntry] = []
for hourOffset in 0..<12 {
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: now)!
entries.append(WeatherEntry(
date: entryDate,
city: city,
temperature: predictTemperature(at: entryDate),
condition: predictCondition(at: entryDate)
))
}
// 关键:告诉系统什么时候来取新的 timeline
let nextUpdate = Calendar.current.date(byAdding: .hour, value: 12, to: now)!
let timeline = Timeline(entries: entries, policy: .after(nextUpdate))
completion(timeline)
}
func predictTemperature(at date: Date) -> Int {
// 简化的温度预测逻辑
let hour = Calendar.current.component(.hour, from: date)
return 20 + (hour > 12 ? (24 - hour) : hour) // 正午最高
}
func predictCondition(at date: Date) -> WeatherCondition {
return .sunny // 简化
}
}
// Widget Entry 和 View
struct WeatherEntry: TimelineEntry {
let date: Date
let city: String
let temperature: Int
let condition: WeatherCondition
}
enum WeatherCondition {
case sunny, cloudy, rainy
}
struct WeatherWidgetEntryView: View {
let entry: WeatherEntry
var body: some View {
VStack(spacing: 8) {
Text(entry.city)
.font(.headline)
Text("\(entry.temperature)°")
.font(.system(size: 48, weight: .bold, design: .rounded))
Text(conditionText)
.font(.caption)
.foregroundColor(.secondary)
}
.padding()
}
var conditionText: String {
switch entry.condition {
case .sunny: return "晴天"
case .cloudy: return "多云"
case .rainy: return "下雨"
}
}
}
// 在主应用中触发 Widget 刷新
import WidgetKit
class WeatherApp {
func refreshWidget() {
// 当主应用获取到新的天气数据时,刷新 Widget
WidgetCenter.shared.reloadTimelines(ofKind: "WeatherWidget")
}
// 获取所有 Widget 的信息
func inspectWidgets() {
WidgetCenter.shared.getCurrentConfigurations { widgetInfos in
guard case .success(let infos) = widgetInfos else { return }
for info in infos {
print("Widget: \(info.kind), Family: \(info.family)")
}
}
}
}
最佳实践
- Timeline entry 的密度要与数据变化的频率匹配:交易时段密集,非活跃时段稀疏
- 使用
.after(date)策略告诉系统何时刷新,而不是.atEnd(后者可能导致刷新频率失控) - 在
getSnapshot()中提供有意义的数据,这是 Widget Gallery 中的第一印象 - 用户配置的 Intent 参数会持久化,不需要在每次 timeline 请求时重新获取
- 测试 Timeline 策略时使用 Xcode 的 Widget Simulator,可以看到 entry 的时间线分布
还有什么值得关注
- Widget 的刷新预算由系统管理,具体次数不公开——Apple 说”足够大多数应用使用”
WidgetCenter还提供了reloadAllTimelines()方法,适合数据模型发生全局变化时使用- Session 提到 Stack Widget(智能叠放)会根据用户习惯自动轮换不同的 Widget,你的 Widget 需要为
isSnapshot场景优化渲染速度