在 iOS18 中,你可以将应用的控件扩展到系统级别,使其出现在控制中心、锁定屏幕等位置。本文将详细介绍如何使用 WidgetKit 构建和定制控件,并让控件支持配置,最终将其添加到系统界面中。

新版控制中心

一、前期准备

在开始之前,确保你已经在 Xcode 中创建了一个 iOS 项目,并安装了最新版本的 Xcode。你还需要一些基本的 Swift 和 SwiftUI 知识。

二、将控件添加到 Widget Bundle

1. 什么是 Widget Bundle?

Widget Bundle 是一种容器,允许你将多个控件组合在一起。通过将控件添加到 Widget Bundle,你可以更好地组织和管理这些控件,并方便地在应用中启用或禁用它们。

2. 创建一个 Widget Bundle

我们需要定义一个 WidgetBundle 来包含我们的控件。以下是具体的代码示例:

1
2
3
4
5
6
7
8
9
10
@main
struct ProductivityExtensionBundle: WidgetBundle {

var body: some Widget {
ChecklistWidget() // 添加清单控件
TaskCounterWidget() // 添加任务计数控件
TimerToggle() // 添加定时器切换控件
}

}
  • @main 标记:表示这是应用的入口点。它将这个 WidgetBundle 注册为应用的一部分。
  • ProductivityExtensionBundle:这是我们定义的 WidgetBundle 名称。
  • var body: some Widget:这个属性表示该 WidgetBundle 中包含的控件列表。你可以在这里添加任意数量的控件。

3. 控件的添加

body 中,我们添加了三个控件:

  • ChecklistWidget:一个清单控件,用于显示任务列表。
  • TaskCounterWidget:一个任务计数控件,用于显示当前任务的数量。
  • TimerToggle:一个定时器切换控件,用于控制定时器的启动和停止。

通过这种方式,你可以将多个控件打包在一起,使它们可以在应用的不同部分方便地使用。

三、构建控件

1. 创建控件的基础结构

我们将使用 ControlWidget 协议来定义一个基础控件。这里我们以定时器切换控件为例进行说明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct TimerToggle: ControlWidget {
var body: some ControlWidgetConfiguration {
StaticControlConfiguration(
kind: "com.apple.Productivity.TimerToggle"
) {
ControlWidgetToggle(
"Work Timer",
isOn: TimerManager.shared.isRunning,
action: ToggleTimerIntent()
) { isOn in
Image(systemName: isOn
? "hourglass"
: "hourglass.bottomhalf.filled")
}
}
}
}
  • struct TimerToggle: ControlWidget:定义了一个新的结构体 TimerToggle,它实现了 ControlWidget 协议。
  • StaticControlConfiguration:这是一个静态配置,用于定义控件的外观和行为。
    • kind:指定控件的唯一标识符,这里使用了 "com.apple.Productivity.TimerToggle"
    • ControlWidgetToggle:定义了一个切换控件,使用 isOn 属性表示当前的状态。
      • isOn:绑定到 TimerManager.shared.isRunning,表示定时器是否在运行。
      • action:指定了一个 ToggleTimerIntent 操作,当用户点击控件时触发该操作。
      • { isOn in ... }:这是一个闭包,用于根据 isOn 的值动态更新控件的图标。

2. 控件的工作原理

这个控件的核心是一个切换按钮。根据定时器的运行状态(isOn),显示不同的图标(如沙漏或空心沙漏),并执行相应的操作(启动或停止定时器)。

3. 控件配置的关键点

  • StaticControlConfiguration:静态配置,用于定义控件的结构。
  • ControlWidgetToggle:切换控件,具有 isOn 属性和操作意图 action

通过这种方式,你可以创建一个简单而功能强大的控件,并根据需要自定义其外观和行为。

四、自定义控件外观

1. 指定不同的符号

为了增强用户体验,我们可以根据定时器的状态显示不同的符号(例如:定时器运行时显示沙漏,停止时显示半沙漏)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct TimerToggle: ControlWidget {
var body: some ControlWidgetConfiguration {
StaticControlConfiguration(
kind: "com.apple.Productivity.TimerToggle"
) {
ControlWidgetToggle(
"Work Timer",
isOn: TimerManager.shared.isRunning,
action: ToggleTimerIntent()
) { isOn in
Image(systemName: isOn
? "hourglass"
: "hourglass.bottomhalf.filled")
}
}
}
}
  • Image(systemName:):这是一个 SwiftUI中的图像控件,用于显示系统图标。
    • isOn:根据 isOn 的值选择不同的图标。
    • systemName: isOn ? "hourglass" : "hourglass.bottomhalf.filled":如果定时器在运行,显示沙漏图标;否则显示半沙漏图标。

2. 添加自定义文本和颜色

你还可以为控件添加自定义文本和颜色,以提供更好的视觉效果和用户提示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct TimerToggle: ControlWidget {
var body: some ControlWidgetConfiguration {
StaticControlConfiguration(
kind: "com.apple.Productivity.TimerToggle"
) {
ControlWidgetToggle(
"Work Timer",
isOn: TimerManager.shared.isRunning,
action: ToggleTimerIntent()
) { isOn in
Label(isOn ? "Running" : "Stopped",
systemImage: isOn
? "hourglass"
: "hourglass.bottomhalf.filled")
}
.tint(.purple)
}
}
}
  • Label:这是一个带有文本和图标的控件,用于显示控件的状态。
    • isOn ? "Running" : "Stopped":根据 isOn 的值选择显示文本,运行时显示“Running”,停止时显示“Stopped”。
    • systemImage:根据 isOn 的值选择不同的图标。
  • .tint(.purple):设置控件的主题色为紫色,使其更具视觉吸引力。

通过这些自定义选项,你可以创建一个更加丰富和直观的控件。

五、实现控件功能

1. 定义定时器切换逻辑

我们需要实现一个操作意图(Intent),用于处理定时器的启动和停止操作。

1
2
3
4
5
6
7
8
9
10
11
struct ToggleTimerIntent: SetValueIntent, LiveActivityIntent {
static let title: LocalizedStringResource = "Productivity Timer"

@Parameter(title: "Running")
var value: Bool // 定时器的运行状态

func perform() throws -> some IntentResult {
TimerManager.shared.setTimerRunning(value)
return .result()
}
}
  • struct ToggleTimerIntent: SetValueIntent, LiveActivityIntent:定义了一个新的结构体 ToggleTimerIntent,它实现了 SetValueIntentLiveActivityIntent 协议。
  • @Parameter:这是一个参数注解,用于定义意图中的参数。
    • title: "Running":参数的标题。
    • var value: Bool:一个布尔值,表示定时器的运行状态。
  • func perform() throws -> some IntentResult:定义了执行意图时的操作。
    • TimerManager.shared.setTimerRunning(value):调用 TimerManager 来设置定时器的运行状态。
    • return .result():返回一个结果,表示意图执行成功。

这个意图允许用户通过控件启动或停止定时器,并将状态更新到控件中。

2. 从应用内部刷新控件

当定时器状态发生变化时,我们需要刷新控件的显示,以确保其显示的状态与实际一致。

1
2
3
4
5
6
func timerManager(_ manager: TimerManager,
timerDidChange timer: ProductivityTimer) {
ControlCenter.shared.reloadControls(
ofKind: "com.apple.Productivity.TimerToggle"
)
}
  • func timerManager(_ manager: TimerManager, timerDidChange timer: ProductivityTimer):这是一个回调函数,当定时器状态发生变化时调用。
  • ControlCenter.shared.reloadControls(ofKind: "com.apple.Productivity.TimerToggle"):调用 ControlCenter 来刷新指定类型的控件,这里是 TimerToggle

通过这种方式,你可以确保控件的显示始终与定时器的实际状态保持一致。

六、值提供者与异步数据获取

1. 定义值提供者

为了提高控件的灵活性和响应性,我们可以使用值提供者(Value Provider)来动态获取控件的状态。

1
2
3
4
5
6
7
8
struct TimerValueProvider: ControlValueProvider {

func currentValue() async throws -> Bool {
try await TimerManager.shared.fetchRunningState()
}

let previewValue: Bool = false
}
  • struct TimerValueProvider: ControlValueProvider:定义了一个新的结构体 TimerValueProvider,它实现了 ControlValueProvider 协议。
  • func currentValue() async throws -> Bool:这是一个异步函数,用于获取当前的定时器状态。
    • try await TimerManager.shared.fetchRunningState():调用 TimerManager 的异步方法,获取定时器的运行状态。
  • let previewValue: Bool = false:定义了一个预览值,当无法获取实际值时使用。

2. 异步获取状态

我们可以将值提供者集成到控件中,以实现异步数据获取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct TimerToggle: ControlWidget {
var body: some ControlWidgetConfiguration {
StaticControlConfiguration(
kind: "com.apple.Productivity.TimerToggle",
provider: TimerValueProvider()
) { isRunning in
ControlWidgetToggle(
"Work Timer",
isOn: isRunning,
action: ToggleTimerIntent()
) { isOn in
Label(isOn ? "Running" : "Stopped",
systemImage: isOn
? "hourglass"
: "hourglass.bottomhalf.filled")
}
.tint(.purple)
}
}
}
  • provider: TimerValueProvider():使用我们定义的 TimerValueProvider 作为值提供者。
  • { isRunning in ... }:这是一个闭包,当获取到定时器状态时执行,其中 isRunning 表示当前的定时器状态。

通过这种方式,你可以使控件动态响应定时器状态的变化,提供更好的用户体验。

七、使控件可配置

1. 定义可配置的值提供者

我们可以进一步扩展值提供者,使其支持配置不同的定时器。

1
2
3
4
5
6
7
8
9
10
11
struct ConfigurableTimerValueProvider: AppIntentControlValueProvider {
func currentValue(configuration: SelectTimerIntent) async throws -> TimerState {
let timer = configuration.timer
let isRunning = try await TimerManager.shared.fetchTimerRunning(timer: timer)
return TimerState(timer: timer, isRunning: isRunning)
}

func previewValue(configuration: SelectTimerIntent) -> TimerState {
return TimerState(timer: configuration.timer, isRunning: false)
}
}
  • struct ConfigurableTimerValueProvider: AppIntentControlValueProvider:定义了一个可配置的值提供者,实现了 AppIntentControlValueProvider 协议。
  • func currentValue(configuration: SelectTimerIntent) async throws -> TimerState:这是一个异步函数,根据配置获取当前的定时器状态。
    • let timer = configuration.timer:获取配置中的定时器。
    • let isRunning = try await TimerManager.shared.fetchTimerRunning(timer: timer):获取指定定时器的运行状态。
  • func previewValue(configuration: SelectTimerIntent) -> TimerState:定义了一个预览值,用于在无法获取实际值时使用。

2. 实现可配置的定时器控件

我们可以使用可配置的值提供者创建一个更加灵活的定时器控件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct TimerToggle: ControlWidget {
var body: some ControlWidgetConfiguration {
AppIntentControlConfiguration(
kind: "com.apple.Productivity.TimerToggle",
provider: ConfigurableTimerValueProvider()
) { timerState in
ControlWidgetToggle(
timerState.timer.name,
isOn: timerState.isRunning,
action: ToggleTimerIntent(timer: timerState.timer)
) { isOn in
Label(isOn ? "Running" : "Stopped",
systemImage: isOn
? "hourglass"
: "hourglass.bottomhalf.filled")
}
.tint(.purple)
}
}
}
  • AppIntentControlConfiguration:这是一个支持应用意图的控件配置,允许根据用户配置动态更新控件。
  • { timerState in ... }:这是一个闭包,根据获取到的定时器状态(timerState)来更新控件。

通过这种方式,你可以创建一个更加灵活和强大的控件,允许用户根据需要配置不同的定时器。

八、自动提示用户配置

1. 自动提示用户配置

为了提高用户体验,我们可以自动提示用户对控件进行配置。

1
2
3
4
5
6
7
8
struct SomeControl: ControlWidget {
var body: some ControlWidgetConfiguration {
AppIntentControlConfiguration(
// 配置项
)
.promptsForUserConfiguration()
}
}
  • .promptsForUserConfiguration():这是一个方法,调用它可以在控件首次使用时自动提示用户进行配置。

通过这种方式,你可以让用户在使用控件时更加方便地进行配置,提升应用的易用性。

九、添加控件提示与描述

1. 添加操作提示和描述

为了让用户更好地理解控件的功能,我们可以为控件添加操作提示和描述。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct TimerToggle: ControlWidget {
var body: some ControlWidgetConfiguration {
AppIntentControlConfiguration(
kind: "com.apple.Productivity.TimerToggle",
provider: ConfigurableTimerValueProvider()
) { timerState in
ControlWidgetToggle(
timerState.timer.name,
isOn: timerState.isRunning,
action: ToggleTimerIntent(timer: timerState.timer)
) { isOn in
Label(isOn ? "Running" : "Stopped",
systemImage: isOn
? "hourglass"
: "hourglass.bottomhalf.filled")
.controlWidgetActionHint(isOn ?
"Start" : "Stop")
}
.tint(.purple)
}
.displayName("Productivity Timer")
.description("Start and stop a productivity timer.")
}
}
  • .controlWidgetActionHint:这是一个方法,用于为控件添加操作提示,如“Start”或“Stop”。
  • .displayName("Productivity Timer"):设置控件的显示名称为“Productivity Timer”。
  • .description("Start and stop a productivity timer."):添加控件的描述,说明其功能是启动和停止生产力定时器。

通过这些提示和描述,你可以让用户更清楚地了解控件的用途和操作方法,提升应用的用户体验。

参考视频

将 App 控件扩展到系统级别