- Published on
- 5 min read
> ControlWidgetButton for Control Center Widgets
iOS 18 lets apps add interactive controls to the Control Center. Users can add your app's buttons and toggles right alongside system controls like flashlight and airplane mode. The ControlWidgetButton template creates buttons that perform actions when tapped.
Setting Up the Widget Extension
Control widgets live in a widget extension, the same type used for Home Screen widgets. If you already have a widget extension, you can add controls to it. Otherwise, create one through File > New > Target > Widget Extension.
Creating a Basic Control Button
A control widget needs three things: the widget definition, an App Intent for the action, and a label. Here's a minimal example:
import WidgetKit
import SwiftUI
import AppIntents
struct QuickNoteControl: ControlWidget {
var body: some ControlWidgetConfiguration {
StaticControlConfiguration(kind: "com.myapp.quicknote") {
ControlWidgetButton(action: CreateNoteIntent()) {
Label("New Note", systemImage: "square.and.pencil")
}
}
.displayName("Quick Note")
.description("Create a new note instantly.")
}
}
The kind string uniquely identifies your control. The displayName and description appear when users browse available controls.
Defining the App Intent
The button's action is defined by an App Intent. This runs when the user taps the control:
import AppIntents
struct CreateNoteIntent: AppIntent {
static var title: LocalizedStringResource = "Create Note"
static var description = IntentDescription("Creates a new empty note.")
static var openAppWhenRun: Bool = true
func perform() async throws -> some IntentResult {
// Your action logic here
NotificationCenter.default.post(
name: .createNewNote,
object: nil
)
return .result()
}
}
extension Notification.Name {
static let createNewNote = Notification.Name("createNewNote")
}
Set openAppWhenRun to true if the action should launch your app. For background actions like toggling a setting, set it to false.
Adding to Your Widget Bundle
Include the control in your widget bundle:
import WidgetKit
import SwiftUI
@main
struct MyAppWidgets: WidgetBundle {
var body: some Widget {
MyHomeScreenWidget()
if #available(iOSApplicationExtension 18.0, *) {
QuickNoteControl()
}
}
}
The availability check ensures the control only appears on iOS 18 and later.
Customizing Appearance
Controls support tint colors and SF Symbols:
struct TimerControl: ControlWidget {
var body: some ControlWidgetConfiguration {
StaticControlConfiguration(kind: "com.myapp.timer") {
ControlWidgetButton(action: StartTimerIntent()) {
Label("Start Timer", systemImage: "timer")
}
.tint(.orange)
}
.displayName("Quick Timer")
.description("Start a 5-minute timer.")
}
}
The tint color affects the icon and button highlight state.
Passing Parameters to Intents
For actions that need context, add parameters to your intent:
struct PlayPlaylistIntent: AppIntent {
static var title: LocalizedStringResource = "Play Playlist"
@Parameter(title: "Playlist Name")
var playlistName: String
init() {
self.playlistName = "Favorites"
}
init(playlistName: String) {
self.playlistName = playlistName
}
func perform() async throws -> some IntentResult {
// Play the specified playlist
MusicPlayer.shared.play(playlist: playlistName)
return .result()
}
}
struct PlayFavoritesControl: ControlWidget {
var body: some ControlWidgetConfiguration {
StaticControlConfiguration(kind: "com.myapp.playfavorites") {
ControlWidgetButton(action: PlayPlaylistIntent(playlistName: "Favorites")) {
Label("Play Favorites", systemImage: "heart.fill")
}
}
.displayName("Play Favorites")
.description("Start playing your favorite songs.")
}
}
Multiple Controls
You can offer several controls for different actions:
@main
struct MusicWidgets: WidgetBundle {
var body: some Widget {
if #available(iOSApplicationExtension 18.0, *) {
PlayFavoritesControl()
PlayRecentControl()
ShuffleAllControl()
}
}
}
Each appears as a separate option in the Control Center customization UI.
Background Actions
For actions that don't need to open the app, perform work directly in the intent:
struct ToggleDarkModeIntent: AppIntent {
static var title: LocalizedStringResource = "Toggle Dark Mode"
static var openAppWhenRun: Bool = false
func perform() async throws -> some IntentResult {
let defaults = UserDefaults(suiteName: "group.com.myapp.shared")
let current = defaults?.bool(forKey: "darkMode") ?? false
defaults?.set(!current, forKey: "darkMode")
return .result()
}
}
Use an App Group to share data between your widget extension and main app.
Testing
Controls appear in the Control Center editor. On a device or simulator running iOS 18, open Control Center, tap the plus button, and look for your app's controls. Tap a control to test the action.
Practical Example
Here's a control that opens a specific view in your app:
import WidgetKit
import SwiftUI
import AppIntents
struct OpenScannerControl: ControlWidget {
var body: some ControlWidgetConfiguration {
StaticControlConfiguration(kind: "com.myapp.scanner") {
ControlWidgetButton(action: OpenScannerIntent()) {
Label("Scan", systemImage: "qrcode.viewfinder")
}
.tint(.blue)
}
.displayName("Quick Scan")
.description("Open the QR code scanner.")
}
}
struct OpenScannerIntent: AppIntent {
static var title: LocalizedStringResource = "Open Scanner"
static var openAppWhenRun: Bool = true
func perform() async throws -> some IntentResult {
// Post notification for app to handle
await MainActor.run {
NotificationCenter.default.post(
name: .openScanner,
object: nil
)
}
return .result()
}
}
extension Notification.Name {
static let openScanner = Notification.Name("openScanner")
}
In your main app, observe the notification to navigate to the scanner view when the control is tapped.
Control widgets give users quick access to your app's key actions without opening the full app. Keep the actions simple and fast since users expect controls to work instantly.
Sample Project
Want to see this code in action? Check out the complete sample project on GitHub:
The repository includes a working Xcode project with all the examples from this article, plus unit tests you can run to verify the behavior.
// Continue_Learning
Why Your Image Isn't Showing in a Live Activity
If your Live Activity renders a grey box instead of your image, the problem is almost certainly the image size. Here's how to fix it.
How to Preview Live Activities in SwiftUI
Use the #Preview macro to preview your Live Activity views directly in Xcode without running the full app.
Sensory Feedback and Haptics in SwiftUI
Add haptic feedback to your SwiftUI views using the sensoryFeedback modifier, with built-in support for success, error, impact, and selection feedback.
// Stay Updated
Get notified when I publish new tutorials on Swift, SwiftUI, and iOS development. No spam, unsubscribe anytime.