Widgets – Simple Interactions
We can provide simple controls that can perform actions in our apps, including triggering updates to a widget.
Overview
This is part three of a series on widgets. Catch up with parts one and two.
Widgets may look a lot like SwiftUI but there are limitations on what we can do. For example, we can’t add a button to update some state and expect anything to change. Instead, we need a button that can perform an App Intent, which can update some data and/or trigger the widget to update.
- Draw a widget with a button
- When the button is tapped, perform an App Intent
- Inside the App Intent we can modify any data, then ask WidgetKit to update our widget
- The content of the widget will refresh, giving is a short period of time when we can animate changes (about 2 seconds max).
Let’s make a new widget. The user can pick an emoji in the configuration screen. We’ll repeat that emoji N times in a radial layout. Then we’ll add two buttons to document and increment a count (N). For the sake of simplicity we’ll store the data in UserDefaults. In a real app, widgets may need to updated data from SwiftData, Core Data, etc.
An App Intent to increment a value and update a widget.
struct IncrementCountIntent: AppIntent {
static var title: LocalizedStringResource = "Increment Count"
static var description: IntentDescription = "Increase the emoji count by 1"
func perform() async throws -> some IntentResult {
// Get current count from UserDefaults and increment
let currentCount = UserDefaults.standard.integer(forKey: "EmojiWidgetCount")
let newCount = min(currentCount + 1, 12)
UserDefaults.standard.set(newCount, forKey: "EmojiWidgetCount")
// Reload the widget timeline
WidgetCenter.shared.reloadTimelines(ofKind: "EmojiWidget")
return .result()
}
}An App Intent to increment a value and update a widget.
struct DecrementCountIntent: AppIntent {
static var title: LocalizedStringResource = "Decrement Count"
static var description: IntentDescription = "Decrease the emoji count by 1"
func perform() async throws -> some IntentResult {
// Get current count from UserDefaults and decrement
let currentCount = UserDefaults.standard.integer(forKey: "EmojiWidgetCount")
let newCount = max(currentCount - 1, 1)
UserDefaults.standard.set(newCount, forKey: "EmojiWidgetCount")
// Reload the widget timeline
WidgetCenter.shared.reloadTimelines(ofKind: "EmojiWidget")
return .result()
}
}We’ll add a control to edit the emoji in another App Intent.
struct EmojiConfigurationAppIntent: WidgetConfigurationIntent {
static var title: LocalizedStringResource { "Emoji Configuration" }
static var description: IntentDescription { "Choose an emoji to display" }
@Parameter(title: "Emoji", default: "🌟")
var emoji: String
}Now we can dive into the Widget itself. We’re going to duplicate a lot of the boilerplate code from the Xcode template we used for the first widget. Check the repo for the full code. We’ll use a computed property in the widget view to read the current count from UserDefaults.
struct EmojiWidgetEntryView: View {
var entry: EmojiProvider.Entry
private var storedCount: Int {
let count = UserDefaults.standard.integer(forKey: "EmojiWidgetCount")
return count > 0 ? count : 3 // Default to 3 if no count stored
}
var body: some View {
RadialLayout(angleOffset: .degrees(0)) {
ForEach(0..<max(1, storedCount), id: \.self) { index in
Text(entry.configuration.emoji)
}
}
...
}
}We can add buttons to the view to call the App Intents we created above. We’ll need to use Button(intent:label:).
Button(intent: DecrementCountIntent()) {
Image(systemName: "minus.circle.fill")
}
Button(intent: IncrementCountIntent()) {
Image(systemName: "plus.circle.fill")
}Let’s see it in action.
There are a few things to keep in mind.
- While this seems interactive, it’s not quite the same a SwiftUI content in a regular window. We’re limited on what we can do and the controls we can use.
- Animations work, but only between widget versions. For example, when we set count from 3 to 4, we can animate the layout change from 3 to 4 emoji as long as the animation finishes quickly. It’s sort of like we are flipping from one snapshot to the next. We can animate the change between the two.
- The demo above works well as long as we tap on the buttons. When we tap elsewhere in the widget, visionOS opens the main window for the app. I haven’t figured out a way to prevent that.
You can find the Xcode project for this example in our Projects Repo. Look for StepIntoWidgets.
Download the Xcode project with this and many more examples from Step Into Vision.
Some examples are provided as standalone Xcode projects. You can find those here.


Follow Step Into Vision