文章

iOS 小组件 - 创建可配置的小组件

Tips: 本文是创建基于 SiriKit 的可配置小组件 最新基于 AppIntent 的可配置组件移步Making a configurable widget

桌面小组件分为两种: 静态小组件 & 可配置小组件

静态小组件就是 Xcode 默认创建的小组件, 不再赘述. 这里主要讲可配置小组件.

可配置小组件有两种创建方式: SiriIntent 和 AppIntent(框架本身是 iOS 16 新增, iOS 17 之后可用于小组件配置, 用此框架需要 iOS 最低支持版本为 17), 鉴于当前项目需要从 iOS 14 开始适配, 所以本文主要讲 SiriIntent 的创建方式.

使用 SiriIntent 创建可配置小组件步骤如下:

  1. 向你的 Xcode 项目中添加用于定义可配置属性的自定意图定义。
  2. 在你的小组件中使用 IntentTimelineProvider 将用户的选择整合到你的时间线条目中。
  3. 如果属性依赖于动态数据,则实施意图扩展。

具体操作如下:

创建组件

创建一个小组件是基本操作, 步骤如下, 细节不再赘述

File -> New Target -> widget extension

向 Xcode 添加用于自定义可配置属性的自定意图定义

SiriKit 意图定义文件步骤:

File -> New File -> SiriKit Intent Definition File -> Next. 并在系统提示时存储文件。Xcode 会创建一个新的 .intentdefinition 文件并将它添加到你的项目中。

创建 SiriKit 意图定义文件

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

  1. 将意图定义文件作为目标的成员包含在内。(简单做法就直接作为目标成员包含到所有能用到的 target 中)
  2. 通过将意图的类名称添加到目标属性的 “Supported Intents” 部分,指明要在目标中包含的特定意图。

要添加和配置用户的自定意图:

  1. 在项目导航器中,选择意图文件。Xcode 会显示一个空的意图定义编辑器。
  2. 选取 Editor -> New Intent, 并在 Custom Intents 下选择意图。
  3. 将自定意图的名称更改为 ChoosePosition 。请注意,属性检查器的 Custom Class 栏位显示你在代码中引用这一意图时使用的类名称。在本例中为“ChoosePositionIntent”。
  4. 将 Category 设置为 View 并选中“Intent is eligible for widgets”(意图适用于小组件) 复选框,以指明小组件可以使用这个意图。
  5. 在 Parameters 下方,添加一个名称为 Position 的新参数,这是小组件的可配置设置。

如图: 是创建了选择自定义 Position 的意图, 可以动态选择用户提供的 Position 信息, 如果提供的配置项目是静态列表, 则在新增类型中选择 Enum 类型.

向项目添加意图拓展 (Intents Extension)

在 App 中添加一个意图扩展。当用户编辑小组件时,WidgetKit 会载入这个意图扩展以提供动态信息。步骤如下:

  1. File -> New -> Target -> Intents Extension -> Next
  2. 输入意图扩展的名称,并将 Starting Point 设置为 None
  3. Finish 并 Activate 新方案
  4. 在新目标属性的 General 标签中,在 Supported Intents 部分中添加一个条目并将 Class Name 设置为 ChoosePositionIntent
  5. 在项目导航器中,选择你之前添加的自定意图定义文件
  6. 使用文件检查器将这个定义文件添加至意图扩展目标

NOTE: 意图定义文件应包含在 App/widet/intentExtension 中

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

当 Xcode 创建了意图扩展后,它向你的项目中添加了一个名为 IntentHandler.swift 的文件,其中包含一个名为 IntentHandler 的类。这个类包含一个返回处理程序的方法。你将扩展这一处理程序来为小组件的自定提供值。

Xcode 根据自定意图定义文件生成处理程序必须遵守的协议,即 ChoosePositionIntentHandling。将这一遵从性添加到 IntentHandler 类的声明中

1
2
3
class IntentHandler: INExtension, ChoosePositionIntentHandling {
...
}

当处理程序提供动态选项时,它必须实现一个名为 provide[Type]OptionalCollection(for:with:) 的方法,其中 [Type] 是意图定义文件中的类型的名称. 示例如下:

1
2
3
4
5
6
7
8
9
10
func providePositionOptionsCollection(for intent: ChoosePositionIntent) async throws -> INObjectCollection<Position> {
        // 获取当前小组件是哪个型号
        let position = Position(identifier: "lefttop", display: "左上", subtitle: "您小组件的位置", image: INImage.systemImageNamed("add"))
        let position2 = Position(identifier: "lefttop", display: "左上", subtitle: "您小组件的位置", image: INImage.systemImageNamed("mark"))
        let position3 = Position(identifier: "leftCenter", display: "左中", subtitle: "您小组件的位置 -- 这个是网络图片", image: INImage(url: URL(string: "https://t12.baidu.com/it/u=2944858655,3260611328&fm=58")!, width: 30, height: 40))
        let position4 = Position(identifier: "leftCenter", display: "右边", subtitle: "您小组件的位置-这个是本地图片", image: INImage(named: "icon"))
        
        return INObjectCollection(items: [position, position2, position3, position4])
    }

处理用户自定值

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct SmallIntentWidget: Widget {
    let kind: String = "Intent Widget"
    
    var body: some WidgetConfiguration {
        
        IntentConfiguration(kind: kind, intent: ChoosePositionIntent.self, provider: IntentProvider()) { entry in
            if #available(iOS 17.0, *) {
                widgetEntryView(entry: entry)
                    .containerBackground(.fill.tertiary, for: .widget)
            } else {
                widgetEntryView(entry: entry)
                    .padding()
                    .background()
            }
        }
        .configurationDisplayName("这里是 Intent 组件")
        .description("This is an example widget.")
    }
}

用户编辑小组件后,WidgetKit 会在请求时间线条目时将用户自定值传递给提供程序。你通常会在提供程序生成的时间线条目中包含意图中的相关详细信息.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct IntentProvider: IntentTimelineProvider {
    typealias Entry = SimpleEntry
    typealias Intent = ChoosePositionIntent
    
    func getTimeline(for configuration: Self.Intent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        var entries: [SimpleEntry] = []
        
        let currentDate = Date()
        for hourOffset in 0 ..< 5 {
            let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
            
            // 获取用户传递的值, 并传递给 entry, 最终展示到 widgetView 
            let emoji = configuration.Position?.displayString ?? "没有拿到值???"
            
            let entry = SimpleEntry(date: entryDate, emoji: emoji)
            entries.append(entry)
        }
        
        let timeline = Timeline(entries: entries, policy: .atEnd)
        completion(timeline)
    }
}

THE END.

参考: https://developer.apple.com/cn/documentation/widgetkit/making-a-configurable-widget/

本文由作者按照 CC BY 4.0 进行授权