用 Controls API 把你的 App 功能扩展到控制中心和锁屏
Extend your app's controls across the system
2024年6月10日
一句话判断
iOS 18 的 Controls API 让你的 App 操作(开/关、启动/停止)直接出现在控制中心、锁屏和 Action Button 上——本质上是用 WidgetKit 的架构把”按钮”做成了系统级控件。
这场 Session 讲了什么
iOS 18 在 WidgetKit 框架中新增了 Control 这一控件类型。Control 有两种形态:Button(执行一次性操作,比如深链接到 App 的某个页面)和 Toggle(切换布尔状态,比如开关某个功能)。Control 可以出现在三个系统空间:Control Center(三种尺寸)、Lock Screen、以及 iPhone 15 Pro 的 Action Button。
演讲者 Cliff 用一个”生产力计时器”作为贯穿全场的示例。这个计时器 Control 在锁屏上可以启动、在控制中心可以停止、通过 Action Button 可以切换运行状态——同一套代码,三个入口。Control 底层使用 App Intent 来执行操作,和 interactive widget 的机制一致。你的 App 提供符号图标、标题、tint 颜色等视觉信息,系统负责根据放置位置决定最终的显示样式。
Session 还讲了 Control 的状态管理机制:系统在你 Control 的 App Intent 执行完毕后自动触发 reload,你的 App 也可以通过 ControlCenter API 主动请求刷新,还支持通过 push notification 触发 reload(适合跨设备同步场景)。
值得深挖的点
Control 的状态同步:本地、App 侧、跨设备
Control 的状态管理有三个层次。最简单的是本地状态——用户在 Control Center 点击 toggle,App Intent 的 perform() 方法修改共享容器中的状态,perform() 返回后系统自动 reload Control 来反映新状态。
第二个层次是 App 侧变更。用户可能直接在 App 里切换了状态(比如在生产力 App 里手动开始计时),此时 App 需要调用 ControlCenter.shared.reloadControls(ofKind:) 来通知系统刷新 Control 的显示。如果不做这一步,Control 上显示的状态和实际状态就会不一致。
第三个层次是跨设备同步。如果你的状态存储在服务端,Control 需要通过 push notification 来获取状态更新。这时你需要在 Control 中使用 TimelineEntry 来承载来自服务器的状态数据,push notification 的 payload 会触发系统重新执行 Control 的 body 来获取最新内容。Cliff 特别提到,如果你频繁刷新 Control 进行调试,可以在开发者设置中开启 WidgetKit Developer Mode 来绕过系统的刷新频率限制。
可配置 Control:从 Static 到 Configurable
Session 中先展示了 StaticControlConfiguration(不可配置的 Control),后面升级到了 IntentConfigurable。可配置 Control 允许用户在添加 Control 时选择参数——比如你的计时器 Control 可以让用户选择时长(25 分钟番茄钟还是 5 分钟休息)。配置过程复用了 App Intent 的参数机制,系统会自动生成配置 UI。
可配置 Control 的一个实用场景是:你的 App 有多个设备或多个场景,用户可以在 Control Center 里添加多个 Control 实例,每个实例配置不同的设备或场景。比如智能家居 App,用户可以添加多个灯光 Control,每个控制不同的灯。
代码片段
基础 Toggle Control
// 在已有的 WidgetBundle 中添加 Control
@main
struct ProductivityWidgetBundle: WidgetBundle {
var body: some Widget {
ProductivityWidget()
TimerToggle() // 新增的 Control
}
}
// 定义一个 Toggle 类型的 Control
struct TimerToggle: ControlWidget {
var body: some ControlWidgetConfiguration {
StaticControlConfiguration(kind: "com.example.timer-toggle") {
// 使用 ControlWidgetToggle 创建开关控件
ControlWidgetToggle(isOn: TimerManager.shared.isRunning) { isOn in
// 根据状态切换图标
Label(
isOn ? "运行中" : "已停止",
systemImage: isOn ? "hourglass" : "hourglass.badge.fill"
)
} action: {
// 通过 App Intent 执行操作
ToggleTimerIntent()
}
.tint(.purple) // 自定义 tint 颜色
}
}
}
坑点:Control 在锁屏和小尺寸模式下只显示 symbol,不显示标题和 value text。所以你选择的 SF Symbol 必须能独立传达含义。
使用 App Intent 执行 Control 操作
// Control 的操作通过 App Intent 实现
struct ToggleTimerIntent: SetValueIntent {
static let title: LocalizedStringResource = "切换计时器"
static let description = IntentDescription("开始或停止生产力计时器")
@Parameter(title: "运行状态")
var value: Bool
// SetValueIntent 需要实现 perform 方法
func perform() async throws -> some IntentResult {
// 更新共享容器中的状态
TimerManager.shared.setRunning(value)
// 如果需要,同时管理 Live Activity
if value {
let activityContent = ActivityContent(
state: TimerAttributes.LiveActivityState(remainingSeconds: 1500),
staleDate: nil
)
let attributes = TimerAttributes(totalSeconds: 1500)
try Activity.request(
attributes: attributes,
content: activityContent
)
} else {
let activities = Activity<TimerAttributes>.activities
for activity in activities {
await activity.end(nil, dismissalPolicy: .immediate)
}
}
return .result()
}
}
坑点:perform() 方法返回后系统才会 reload Control,所以所有状态更新必须在 perform() 返回前完成。如果你有异步操作,用 await 等待完成后再返回。
从 App 侧主动刷新 Control
// 在 App 内部状态变化时,通知 Control 刷新
class TimerManager: ObservableObject {
static let shared = TimerManager()
func setRunning(_ running: Bool) {
// 更新共享容器中的状态
UserDefaults(suiteName: "group.com.example.productivity")?
.set(running, forKey: "timerRunning")
// 通知 Control 刷新显示
ControlCenter.shared.reloadControls(
ofKind: "com.example.timer-toggle"
)
}
}
坑点:reloadControls 调用有频率限制,正常使用不会触发,但如果你在循环中频繁调用可能会被系统忽略。开发阶段开启 WidgetKit Developer Mode 可以解除限制。
最佳实践
如果你有 interactive widget 的开发经验,Controls 的学习曲线很低——同样的 WidgetKit 架构,同样的 App Intent 机制。建议的迁移路径:先从一个简单的 Toggle Control 开始,让你的核心开关功能出现在控制中心和锁屏上。验证基本功能后,再考虑加入 Configurable 支持,允许用户自定义 Control 的行为参数。
一个容易忽视的点是 Control 的视觉设计。Control 在不同系统空间中的显示尺寸差异很大——Control Center 有三种尺寸,锁屏上只有一个圆形图标的位置。你的 SF Symbol 必须在小尺寸下也能清晰辨识,tint 颜色要确保在深色和浅色模式下都有足够对比度。不要过度依赖文字来传达信息,因为文字在很多显示场景下是看不到的。
还有什么值得关注
- Control 支持 push notification 触发 reload,适合状态存储在服务端的场景(比如 IoT 设备控制)。
- Action Button 在 iOS 18 对所有开发者开放,不再只是系统功能的专属,你可以让用户把你的 Control 绑定到 Action Button 上。
- Control 和 Live Activity 可以联动——比如计时器 Control 启动后同时触发一个 Live Activity 来显示倒计时。