Widgets Code-along, part 2: Alternate timelines
Swift & UI 进阶 36m

Widget 代码实战 2:多条时间线

Widgets Code-along, part 2: Alternate timelines

2020年6月25日

在 Apple 官方观看视频

一句话判断

这一集深入 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 的显示内容
WWDC 2020