Widget 代码实战 2:多条时间线
Widgets Code-along, part 2: Alternate timelines
2020年6月25日
一句话判断
这一集深入 Timeline 的策略设计——什么时候提供多条 Entry、什么时候请求刷新、如何在不同时间段展示不同内容,这些决策直接影响 Widget 的”智能感”。
这场 Session 讲了什么
承接第一集的角色冒险 Widget,第二集的重点是 Timeline 策略。单条 Timeline 只能让 Widget 显示固定内容,而多条 Entry 可以让 Widget 在不同时间自动展示不同的信息。
Session 展示了如何构建一条覆盖全天的 Timeline:早上显示角色的每日任务、下午显示战斗状态、晚上显示当日成就总结。每条 Entry 都有对应的日期,系统会在合适的时间自动切换。
另一个关键话题是 Timeline Reload Policy。三种策略分别是 .atEnd(当前 Timeline 的最后一条 Entry 过期后重新获取)、.after(date)(在指定日期后重新获取)、以及 .never(不需要自动刷新)。选择正确的策略可以避免浪费刷新预算。
值得深挖的点
Timeline 刷新预算的概念。 系统对每个 Widget 的刷新频率有隐式限制。如果你频繁请求刷新(通过 WidgetCenter.shared.reloadAllTimelines()),系统可能会延迟或跳过某些刷新。合理的做法是提供足够长的 Timeline(覆盖几小时甚至一天),而不是频繁调用 reload。
.atEnd vs .after(date) 的选择。 .atEnd 的优点是自动——Timeline 最后一条 Entry 过期后系统会自动调用 getTimeline。但如果你知道下一个重要时间点(如定时任务完成时间),.after(date) 更精确。
代码片段
// 构建全天候的 Timeline
struct CharacterProvider: TimelineProvider {
func getTimeline(in context: Context, completion: @escaping (Timeline<CharacterEntry>) -> Void) {
let character = loadCharacterFromAppGroup()
let calendar = Calendar.current
let now = Date()
var entries: [CharacterEntry] = []
// 早上:显示每日任务
let morningEntry = CharacterEntry(
date: calendar.startOfDay(for: now),
character: character,
displayMode: .dailyQuest // 新增:显示模式
)
entries.append(morningEntry)
// 下午:战斗状态
if let afternoon = calendar.date(bySettingHour: 14, minute: 0, second: 0, of: now) {
entries.append(CharacterEntry(
date: afternoon,
character: character,
displayMode: .battleStatus
))
}
// 晚上:当日总结
if let evening = calendar.date(bySettingHour: 20, minute: 0, second: 0, of: now) {
entries.append(CharacterEntry(
date: evening,
character: character,
displayMode: .dailySummary
))
}
// 使用 .atEnd 策略,最后一条 Entry 过期后自动刷新
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
// 主 App 中主动触发 Widget 刷新
// 当角色数据发生变化时通知 Widget 更新
func updateCharacter(_ character: Character) {
// 保存到共享容器
let defaults = UserDefaults(suiteName: "group.com.example.adventure")
let data = try? JSONEncoder().encode(character)
defaults?.set(data, forKey: "character")
// 请求刷新 Widget(注意:系统会限制频率)
WidgetCenter.shared.reloadAllTimelines()
}
// 根据显示模式切换 Widget 内容
struct CharacterWidgetEntryView: View {
var entry: CharacterEntry
var body: some View {
switch entry.displayMode {
case .dailyQuest:
DailyQuestView(character: entry.character)
case .battleStatus:
BattleStatusView(character: entry.character)
case .dailySummary:
DailySummaryView(character: entry.character)
}
}
}
最佳实践
- 预生成足够长的 Timeline。 覆盖至少几个小时甚至一天的数据,减少 refresh 请求。
- 在主 App 数据变化时主动 reload。 但不要过度——每次数据小变动都 reload 会浪费预算,在用户退出关键编辑页面时 reload 一次就够了。
- Timeline 的 Entry 不要太多。 几十条就够了,几百条 Entry 没有意义——系统不会频繁刷新到那些未来的 Entry。
- 用
.after(date)指定精确的刷新时间。 如果你知道下次数据变化的确切时间(如比赛开始时间),用.after比.atEnd更高效。
还有什么值得关注
- 系统会在低电量模式下减少 Widget 刷新频率
- 用户很少查看的 Widget 会被降低刷新优先级
- 第三集将介绍用户可配置的 Intent Widget
- 可以通过
WidgetCenter.shared.getCurrentDescriptions检查当前 Widget 的显示内容