WWDC Quick Look 💓 By SwiftGGTeam
Extend your app's controls across the system

Extend your app's controls across the system

观看原视频

Highlight

iOS 18 在 WidgetKit 框架中新增了 Control 控件类型,支持 Button 和 Toggle 两种形态,可出现在控制中心、锁屏和 iPhone 15 Pro 的 Action 按钮三个系统空间中。

核心内容

iOS 14 引入的 WidgetKit 让 App 能够展示丰富信息,比如天气或日历事件。iOS 18 扩展了这一框架,新增了 Control 控件类型。Control 专注于提供快速操作入口,比如开关手电筒或深链到时钟 App。

Control 分为两种类型:Button 执行一次性操作(可包括启动 App),Toggle 切换布尔状态(比如开关某项功能)。与 interactive widget 类似,Control 也使用 App Intent 来执行操作。你的 App 提供符号图标、标题、tint 颜色等视觉信息,系统根据放置位置决定最终显示样式。

演讲者 Cliff 以一个”生产力计时器”为例展示 Control 的开发流程。这个计时器可以在锁屏上启动、在控制中心停止、通过 Action 按钮切换运行状态——同一套代码,三个入口。Control 的状态管理机制:系统会在 App Intent 执行完毕后自动触发 reload,App 也可以通过 ControlCenter API 主动请求刷新,还支持通过 push notification 触发 reload 来实现跨设备同步。

详细内容

基础 Control 构建

构建 Control 的第一步是将其添加到现有的 Widget Bundle 中(03:13):

@main
struct ProductivityExtensionBundle: WidgetBundle {
    
    var body: some Widget {
        ChecklistWidget()
        TaskCounterWidget()
        TimerToggle()
    }
    
}

关键点:

  • TimerToggle() 是新增的 Control 条目,与现有 Widget 共享同一个 Widget Bundle
  • WidgetKit 的架构让 Control 可以和 Widget 共存于同一扩展中

接下来定义 Control 本体(03:29):

struct TimerToggle: ControlWidget {
    var body: some ControlWidgetConfiguration {
        StaticControlConfiguration(
            kind: "com.apple.Productivity.TimerToggle"
        ) {
            ControlWidgetToggle(
                "Work Timer",
                isOn: TimerManager.shared.isRunning,
                action: ToggleTimerIntent()
            ) { _ in
                Image(systemName:
                      "hourglass.bottomhalf.filled")
            }
        }
    }
}

关键点:

  • ControlWidget 是 Control 遵循的协议
  • StaticControlConfiguration 用于不可配置的 Control
  • kind 参数是 Control 的唯一标识符,反向 DNS 风格
  • ControlWidgetToggle 定义这是一个 Toggle 类型的控件
  • isOn 绑定到 TimerManager 的运行状态
  • ToggleTimerIntent() 是点击时执行的 App Intent
  • 闭包返回的 Image 定义了 Control 显示的符号

状态化视觉设计

Control 可以根据状态显示不同的符号(04:41):

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")
            }
        }
    }
}

关键点:

  • 闭包参数 isOn 让你可以根据状态返回不同的视觉内容
  • 运行时显示流动的沙漏符号,停止时显示半填充沙漏
  • 系统会在状态变化时自动重新计算闭包返回值

进一步自定义 value text 和 tint 颜色(05:21):

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 替代 Image 可以同时显示文字和符号
  • isOn ? "Running" : "Stopped" 自定义了状态文本,替代默认的 “on/off”
  • .tint(.purple) 设置开启状态下的颜色,替代默认的 systemBlue
  • value text 在锁屏和小尺寸控制中心中不可见,只有符号显示

App Intent 实现

Control 的操作通过 App Intent 执行(08:14):

struct ToggleTimerIntent: SetValueIntent, LiveActivityIntent {
    static let title: LocalizedStringResource = "Productivity Timer"
    
    @Parameter(title: "Running")
    var value: Bool  // The timer's running state
    
    func perform() throws -> some IntentResult {
        TimerManager.shared.setTimerRunning(value)
        return .result()
    }
}

关键点:

  • SetValueIntent 协议表示这个 Intent 用于设置某个值
  • LiveActivityIntent 协议表示它会修改 Live Activity
  • @Parameter 标注的 value 会由系统根据当前状态自动填充
  • perform() 方法在返回前必须完成所有状态更新
  • 系统会在 perform() 返回后自动 reload Control

主动刷新 Control

当 App 内部状态变化时,可以主动请求刷新 Control(08:54):

func timerManager(_ manager: TimerManager,
                  timerDidChange timer: ProductivityTimer) {
    ControlCenter.shared.reloadControls(
        ofKind: "com.apple.Productivity.TimerToggle"
    )
}

关键点:

  • ControlCenter.shared.reloadControls(ofKind:) 请求系统刷新指定 Control
  • 适用于 App 内状态变化需要同步到 Control 的场景
  • 开发时可开启 WidgetKit Developer Mode 移除系统刷新策略

异步状态获取

当状态需要从服务器或数据库异步获取时,使用 ControlValueProvider10:03):

struct TimerValueProvider: ControlValueProvider {
    
    func currentValue() async throws -> Bool {
        try await TimerManager.shared.fetchRunningState()
    }
    
    let previewValue: Bool = false
}

关键点:

  • currentValue() 是异步函数,可从数据库或服务器获取状态
  • 可以抛出错误来表示状态暂时无法计算
  • previewValue 用于控件预览场景(如图库、锁屏自定义),应快速返回且对应关闭状态

使用 ValueProvider 的 Control 初始化(11:00):

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 参数传入 ValueProvider 实例
  • 异步获取的值通过闭包参数 isRunning 传递
  • 系统会先执行 ValueProvider,再用返回值生成 Control 内容

可配置 Control

让 Control 支持用户配置,使用 AppIntentControlValueProvider13:06):

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)
    }
}

关键点:

  • AppIntentControlValueProvider 让值依赖于 Intent 配置
  • SelectTimerIntent 允许用户选择要控制的计时器
  • 返回的 TimerState 是自定义结构体,包含计时器和其运行状态

对应的 Control 配置(13:40):

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 用于可配置的 Control
  • timerState 是自定义结构体,包含更丰富的状态信息
  • Control 标题、操作目标都根据配置动态变化

强制用户配置(14:26):

struct SomeControl: ControlWidget {
    var body: some ControlWidgetConfiguration {
        AppIntentControlConfiguration(
            // ...
        )
        .promptsForUserConfiguration()
    }
}

关键点:

  • promptsForUserConfiguration() 让系统在添加 Control 时自动弹出配置界面
  • 适用于需要配置才能正常工作的 Control

界面微调

自定义 Action Button 的操作提示(15:42):

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)
        }
    }
}

关键点:

  • controlWidgetActionHint 自定义 Action Button 的提示文本
  • 提示应以动词开头,系统会自动添加 “Hold to” 前缀
  • isOn ? "Start" : "Stop" 让提示变为 “Hold to Start” 和 “Hold to Stop”

设置显示名称和描述(16:56):

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.")
    }
}

关键点:

  • displayName 设置 Control 的显示名称,替代默认的 App 名称
  • description 在配置界面显示,帮助用户理解 Control 的功能
  • 每个 Control 都应设置具体的 displayName

核心启发

  • 为高频操作添加 Control 入口:找出用户最常执行的一两个操作(比如开始专注、切换白名单、快速记录),为它们创建 Toggle 或 Button Control。不要试图把整个 App 功能塞进去,Control 应该是单一功能的快速入口。

  • 从不可配置的 Toggle 开始:先用 StaticControlConfiguration 构建一个最简单的 Toggle Control,验证核心功能。等基础体验跑通后,再考虑 AppIntentControlConfiguration 支持用户配置多个实例。这样能避免一次性引入太多复杂度。

  • 设计适合小尺寸的符号:Control 在锁屏上只显示一个圆形图标,在控制中心小尺寸下也只有符号空间。确保你的 SF Symbol 在极小尺寸下仍能清晰辨识,不要依赖文字来传达信息。tint 颜色在深浅模式下都要有足够对比度。

  • 善用 ValueProvider 支持异步状态:如果你的状态需要从网络或数据库获取,使用 ControlValueProvider 而不是在 body 中同步获取。previewValue 应该是一个固定的、快速返回的值,通常对应 Control 的关闭状态。

  • 主动刷新保持状态同步:当用户在 App 内部修改了状态时,使用 ControlCenter.shared.reloadControls(ofKind:) 主动刷新 Control。对于跨设备场景,可以配置 push notification 在收到状态变更推送时自动 reload。

关联 Session

评论

GitHub Issues · utterances