文章

制作可配置小组件

通过向你的项目中添加自定 SiriKit 意图定义,为用户提供自定小组件的选项。

最新英文文章

Making a Configurable Widget

概览

为让用户能够轻松获取最相关的信息,小组件可以提供可自定属性。例如,用户可以为股票报价小组件选择一只特定的股票,或为包裹递送小组件输入一个物流跟踪编号。小组件使用自定意图定义来定义可自定属性,这与 Siri 建议和 Siri 快捷指令用于自定这些交互的机制相同。

要向你的小组件添加可配置属性:

  1. 向你的 Xcode 项目中添加用于定义可配置属性的自定意图定义。

  2. 在你的小组件中使用 IntentTimelineProvider 将用户的选择整合到你的时间线条目中。

  3. 如果属性依赖于动态数据,则实施意图扩展。

如果你的 App 已支持 Siri 建议或 Siri 快捷指令,并且你有自定意图,那么你可能已经完成了大部分工作。否则,请考虑利用你为小组件所做的工作来添加对 Siri 建议或 Siri 快捷指令的支持。如需进一步了解如何充分利用意图,请参阅“SiriKit”。

下一部分介绍了如何将可配置属性添加到显示有关游戏中角色的信息的小组件。

向你的项目中添加自定意图定义

在你的 Xcode 项目中,选取“File”(文件) >“New File”(新建文件),然后选择“SiriKit Intent Definition File”(SiriKit 意图定义文件)。点按“Next”(下一步) 并在系统提示时存储文件。Xcode 会创建一个新的 .intentdefinition 文件并将它添加到你的项目中。

截屏显示了 Xcode 的新建文件表单,其中“SiriKit Intent Definition File” (SiriKit 意图定义文件) 模板处于选中状态

Xcode 从意图定义文件生成代码。要在目标中使用这一代码:

  • 将意图定义文件作为目标的成员包含在内。

  • 通过将意图的类名称添加到目标属性的“Supported Intents”(支持的意图) 部分,指明要在目标中包含的特定意图。

如果你将意图定义文件包含在框架中,则必须确保在含有 App 的目标里也包含该意图。在这种情况下,为避免在 App 和框架中复制类型,请在文件检查器的“Target Membership”(目标成员资格) 部分中为 App 目标选择“No Generated Classes”(无生成的类)。

要添加和配置允许用户在游戏中选择角色的自定意图:

  1. 在项目导航器中,选择意图文件。Xcode 会显示一个空的意图定义编辑器。

  2. 选取“Editor”(编辑器) >“New Intent”(新意图),并在“Custom Intents”(自定意图) 下选择意图。

  3. 将自定意图的名称更改为“SelectCharacter”。请注意,属性检查器的“Custom Class”(自定类) 栏位显示你在代码中引用这一意图时使用的类名称。在本例中为“SelectCharacterIntent”。

  4. 将“Category”(类别) 设置为“View”(视图) 并选中“Intent is eligible for widgets”(意图适用于小组件) 复选框,以指明小组件可以使用这个意图。

  5. 在“Parameters”(参数) 下方,添加一个名称为 character 的新参数,这是小组件的可配置设置。

截屏显示了 Xcode 的意图定义编辑器中的自定意图和参数

在你添加参数后,为其配置详细信息。如果参数为用户提供一个静态选项列表,选取“Add Enum”(添加枚举) 菜单项来创建静态枚举。例如,如果参数用于为角色指定头像,并且可能的头像列表是一个不会改变的常量集,那么你可以在意图定义文件中使用静态枚举来指定可用选项。如果可能的头像列表会发生改变或以动态方式生成,应改用提供动态选项的类型。

在这个示例中,character 属性依赖于 App 中提供的角色的动态列表。要提供动态数据,请创建一个新类型:

  1. 从“Type”(类型) 弹出式菜单中,选取“Add Type”(添加类型)。Xcode 会在编辑器的“Types”(类型) 部分中添加一个新类型。

  2. 将这个类型的名称更改为 GameCharacter

  3. 添加一个新的 name 属性,并从“Type”(类型) 弹出式菜单中选取“String”(字符串)。

  4. 选择 SelectCharacter 意图。

  5. 在意图编辑器中,选中“Options are provided dynamically”(选项以动态方式提供) 复选框,以指明你的代码为这个参数提供动态项目列表。

截屏显示在 Xcode 的意图定义编辑器中添加了自定类型

GameCharacter 类型描述用户可以选取的角色。在下一部分中,你将添加代码以动态提供角色列表。

向你的项目中添加意图扩展

要动态提供角色列表,你将需要向你的 App 中添加一个意图扩展。当用户编辑小组件时,WidgetKit 会载入这个意图扩展以提供动态信息。要添加意图扩展:

  1. 选取“File”(文件) >“New”(新建) >“Target”(目标),并选取意图扩展。

  2. 点按“Next”(下一步)。

  3. 输入意图扩展的名称,并将“Starting Point”(起点) 设置为“None”(无)。

  4. 点按“Finish”(完成)。如果 Xcode 提示你激活新方案,请点按“Activate”(激活)。

  5. 在新目标属性的“General”(通用) 标签中,在“Supported Intents”(支持的意图) 部分中添加一个条目并将“Class Name”(类名称) 设置为 SelectCharacterIntent

  6. 在项目导航器中,选择你之前添加的自定意图定义文件。

  7. 使用文件检查器将这个定义文件添加至意图扩展目标。

实现意图处理程序以提供动态值

当用户对包含自定意图且提供动态值的小组件进行编辑时,系统需要一个对象来提供这些值。它通过要求意图扩展为意图提供处理程序来识别这个对象。当 Xcode 创建了意图扩展后,它向你的项目中添加了一个名为 IntentHandler.swift 的文件,其中包含一个名为 IntentHandler 的类。这个类包含一个返回处理程序的方法。你将扩展这一处理程序来为小组件的自定提供值。

Xcode 根据自定意图定义文件生成处理程序必须遵守的协议,即 SelectCharacterIntentHandling。将这一遵从性添加到 IntentHandler 类的声明中。(如需查看这个协议的详细信息以及 Xcode 自动生成的其他类型,请选择 SelectCharacterIntentHandling,然后选取“Navigate”(导航) >“Jump to Definition”(跳转至定义)。)


class IntentHandler: INExtension, SelectCharacterIntentHandling {
    ...
}

当处理程序提供动态选项时,它必须实现一个名为 provide[Type]OptionalCollection(for:with:) 的方法,其中 [Type] 是意图定义文件中的类型的名称。如果缺少这个方法,Xcode 会报告一个构建错误,同时提供添加协议存根的修复建议。构建你的项目,然后使用这一修复建议来添加该存根。

这个方法包含一个可供你调用的 completion 处理程序,传递 INObjectCollection<GameCharacter>。注意 GameCharacter 类型;这是意图定义文件中的自定类型。Xcode 生成代码来对它进行定义,如下所示:


public class GameCharacter: INObject {
    @available(iOS 13.0, macOS 11.0, watchOS 6.0, *)
    @NSManaged public var name: String?
}

注意 name 属性,它也来自你添加的适用于自定类型的意图定义文件。

为实现 provideCharacterOptionsCollection(for:with:) 方法,小组件使用游戏项目中存在的一个结构。这个结构定义了可用角色及其详细信息的列表,如下所示:


struct CharacterDetail {
    let name: String
    let avatar: String
    let healthLevel: Double
    let heroType: String


    static let availableCharacters = [
        CharacterDetail(name: "Power Panda", avatar: "🐼", healthLevel: 0.14, heroType: "Forest Dweller"),
        CharacterDetail(name: "Unipony", avatar: "🦄", healthLevel: 0.67, heroType: "Free Rangers"),
        CharacterDetail(name: "Spouty", avatar: "🐳", healthLevel: 0.83, heroType: "Deep Sea Goer")
    ]
}

在意图处理程序中,代码遍历 availableCharacters 数组,为每个角色创建一个 GameCharacter 对象。为简单起见,GameCharacter 的标识就是角色的名称。游戏角色数组被放进 INObjectCollection 中,然后处理程序将这个集合传递给完成处理程序。


class IntentHandler: INExtension, SelectCharacterIntentHandling {
    func provideCharacterOptionsCollection(for intent: SelectCharacterIntent, with completion: @escaping (INObjectCollection<GameCharacter>?, Error?) -> Void) {


        // Iterate the available characters, creating
        // a GameCharacter for each one.
        let characters: [GameCharacter] = CharacterDetail.availableCharacters.map { character in
            let gameCharacter = GameCharacter(
                identifier: character.name,
                display: character.name
            )
            gameCharacter.name = character.name
            return gameCharacter
        }


        // Create a collection with the array of characters.
        let collection = INObjectCollection(items: characters)


        // Call the completion handler, passing the collection.
        completion(collection, nil)
    }
}

意图定义文件配置完成且意图扩展已添加至 App 后,用户就可以编辑小组件以选择要显示的特定角色。WidgetKit 使用意图定义文件中的信息来自动创建用于编辑小组件的用户界面。

在用户编辑小组件并选择了一个角色后,下一步就是将这一选择整合到小组件的显示中。

处理用户自定值

为支持可配置的属性,小组件使用 IntentTimelineProvider 配置。例如,角色详细信息小组件按如下所示定义其配置:


struct CharacterDetailWidget: Widget {
    var body: some WidgetConfiguration {
        IntentConfiguration(
            kind: "com.mygame.character-detail",
            intent: SelectCharacterIntent.self,
            provider: CharacterDetailProvider(),
        ) { entry in
            CharacterDetailView(entry: entry)
        }
        .configurationDisplayName("Character Details")
        .description("Displays a character's health and other details")
        .supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
    }
}

SelectCharacterIntent 参数决定小组件的用户可自定属性。配置使用 CharacterDetailProvider 来管理小组件的时间线事件。有关时间线提供程序的更多信息,请参阅“让小组件保持最新状态”。

用户编辑小组件后,WidgetKit 会在请求时间线条目时将用户自定值传递给提供程序。你通常会在提供程序生成的时间线条目中包含意图中的相关详细信息。在这个示例中,提供程序采用帮助程序方法按意图中的角色名称来查找 CharacterDetail,然后创建一个时间线以及一个包含角色详细信息的条目:


struct CharacterDetailProvider: IntentTimelineProvider {
    func getTimeline(for configuration: SelectCharacterIntent, in context: Context, completion: @escaping (Timeline<CharacterDetailEntry>) -> Void) {
        // Access the customized properties of the intent.
        let characterDetail = lookupCharacterDetail(for: configuration.character.name)


        // Construct a timeline entry for the current date, and include the character details.
        let entry = CharacterDetailEntry(date: Date(), detail: characterDetail)


        // Create the timeline and call the completion handler. The .never reload 
        // policy indicates that the containing app will use WidgetCenter methods 
        // to reload the widget's timeline when the details change.
        let timeline = Timeline(entries: [entry], policy: .never)
        completion(timeline)
    }
}

当你在时间线条目中包含用户自定值时,你的小组件可以显示相应的内容。

另请参阅

可配置小组件