BS
BleepingSwift
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:

View 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.

subscribe.sh

// Stay Updated

Get notified when I publish new tutorials on Swift, SwiftUI, and iOS development. No spam, unsubscribe anytime.

>

By subscribing, you agree to our Privacy Policy.