Widget 代码实战 3:进阶时间线
Widgets Code-along, part 3: Advancing timelines
2020年6月25日
一句话判断
系列最终集引入了 IntentConfiguration——让用户在 Widget 上长按就能配置显示内容(比如选择追踪哪个角色),这是 Widget 从”信息展示”升级为”个性化工具”的关键。
这场 Session 讲了什么
前两集的 Widget 使用 StaticConfiguration,内容固定不变。第三集将其升级为 IntentConfiguration,允许用户通过长按 Widget 进入配置界面,选择想要追踪的角色。
核心步骤包括:在 Xcode 中定义一个 INIntent 子类(用于描述可配置参数)、在 Widget 配置中用 IntentConfiguration 替换 StaticConfiguration、在 Timeline Provider 中读取用户选择的参数来生成对应的 Timeline。
Session 还介绍了如何利用 Siri Intent Definition 文件来定义配置选项的 UI——系统会自动生成配置界面,你只需要提供可选的数据列表。这比自定义配置界面简单得多。
值得深挖的点
Intent 定义的参数类型。 Siri Intent Definition 支持多种参数类型:字符串、数字、日期、以及自定义类型(如”角色”)。自定义类型可以包含多个属性(名字、头像等),系统会自动生成对应的选取界面。
Intent Timeline Provider 的额外方法。 IntentTimelineProvider 比 TimelineProvider 多了一个 getSnapshot(for:in:completion:) 方法,其中 for 参数就是用户的 Intent。在 Widget Gallery 中预览时,系统会传入一个默认的 Intent,你需要返回一个有代表性的预览数据。
代码片段
import WidgetKit
import SwiftUI
import Intents
// Intent Timeline Provider
struct CharacterIntentProvider: IntentTimelineProvider {
func placeholder(in context: Context) -> CharacterEntry {
CharacterEntry(
date: Date(),
character: Character(name: "勇士", level: 5, health: 80, maxHealth: 100),
displayMode: .dailyQuest
)
}
func getSnapshot(
for configuration: CharacterSelectionIntent,
in context: Context,
completion: @escaping (CharacterEntry) -> Void
) {
// Widget Gallery 预览时使用用户选择或默认角色
let character = character(from: configuration) ?? defaultCharacter
completion(CharacterEntry(date: Date(), character: character, displayMode: .dailyQuest))
}
func getTimeline(
for configuration: CharacterSelectionIntent,
in context: Context,
completion: @escaping (Timeline<CharacterEntry>) -> Void
) {
// 根据用户选择的角色生成 Timeline
let character = character(from: configuration) ?? defaultCharacter
let entry = CharacterEntry(date: Date(), character: character, displayMode: .dailyQuest)
let timeline = Timeline(entries: [entry], policy: .atEnd)
completion(timeline)
}
private func character(from intent: CharacterSelectionIntent) -> Character? {
guard let name = intent.characterName else { return nil }
return loadCharacter(named: name)
}
}
// 使用 IntentConfiguration 替换 StaticConfiguration
struct CharacterWidget: Widget {
let kind: String = "CharacterWidget"
var body: some WidgetConfiguration {
IntentConfiguration(
kind: kind,
intent: CharacterSelectionIntent.self, // 自定义 Intent 类型
provider: CharacterIntentProvider()
) { entry in
CharacterWidgetEntryView(entry: entry)
}
.configurationDisplayName("角色追踪")
.description("追踪你最喜欢的角色状态")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
}
}
// 提供 Intent 选项数据(在 IntentHandler 中)
class IntentHandler: INExtension, CharacterSelectionIntentHandling {
func provideCharacterOptionsCollection(
for intent: CharacterSelectionIntent,
with completion: @escaping (INObjectCollection<CharacterOption>, Error?) -> Void
) {
// 从 App Group 加载角色列表
let characters = loadAllCharacters()
let options = characters.map { character in
CharacterOption(
identifier: character.name,
display: character.name
)
}
completion(INObjectCollection(items: options), nil)
}
}
最佳实践
- 为 Intent 提供有意义的默认值。 用户首次添加 Widget 时可能不会主动配置,默认值应该展示最有代表性的内容。
- Intent 选项列表要精简。 不要提供几十个选项,限制在最常用的 5-10 个,避免选择困难。
- 配置变化时立即刷新 Timeline。 用户更改 Widget 配置后,系统会自动调用
getTimeline,确保新的 Timeline 基于最新的配置生成。 - 在 Intent Definition 中做好参数的本地化。 配置界面的文字需要支持多语言。
还有什么值得关注
- Intent Configuration 可以支持多个参数(如角色 + 显示模式)
- 用户可以添加多个相同 Widget 实例,每个使用不同配置
- 系统的配置界面自动适配浅色/深色模式
- Intent Handler 运行在独立进程中,也需要通过 App Group 访问数据