Widgets Code-along, part 3: Advancing timelines
Swift & UI 进阶 36m

Widget 代码实战 3:进阶时间线

Widgets Code-along, part 3: Advancing timelines

2020年6月25日

在 Apple 官方观看视频

一句话判断

系列最终集引入了 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 的额外方法。 IntentTimelineProviderTimelineProvider 多了一个 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 访问数据
WWDC 2020