How to Implement App Shortcuts in SwiftUI
Conform a struct to AppIntent to describe the action, then expose it to Siri and the Shortcuts app by listing it inside an AppShortcutsProvider. No configuration profile or entitlement is required — the system discovers your shortcuts automatically at launch.
import AppIntents
struct LogWaterIntent: AppIntent {
static let title: LocalizedStringResource = "Log Water"
func perform() async throws -> some IntentResult & ProvidesDialog {
WaterStore.shared.log(ml: 250)
return .result(dialog: "Logged 250 ml of water!")
}
}
struct WaterShortcuts: AppShortcutsProvider {
static var appShortcuts: [AppShortcut] {
AppShortcut(
intent: LogWaterIntent(),
phrases: ["Log water in \(.applicationName)"],
shortTitle: "Log Water",
systemImageName: "drop.fill"
)
}
}
Full implementation
The example below wires up a hydration-tracking app with a parameterised LogWaterIntent that accepts an amount in millilitres. The WaterShortcuts provider registers three natural-language Siri phrases, and the SwiftUI view surfaces a ShortcutsLink button so users can add the action to Siri without leaving the app. The data layer is a simple observable store — swap in SwiftData or your own persistence as needed.
import SwiftUI
import AppIntents
// MARK: - Data store (swap for SwiftData in production)
@Observable
final class WaterStore {
static let shared = WaterStore()
private(set) var totalML: Int = 0
func log(ml: Int) {
totalML += ml
}
}
// MARK: - AppIntent
struct LogWaterIntent: AppIntent {
static let title: LocalizedStringResource = "Log Water"
static let description = IntentDescription(
"Adds a drink to your daily water total.",
categoryName: "Health"
)
/// Siri will ask "How many millilitres?" if omitted.
@Parameter(title: "Amount (ml)", default: 250, inclusiveRange: (50, 2000))
var amount: Int
func perform() async throws -> some IntentResult & ProvidesDialog {
WaterStore.shared.log(ml: amount)
return .result(dialog: "Logged \(amount) ml. Keep it up!")
}
}
// MARK: - AppShortcutsProvider
struct WaterShortcuts: AppShortcutsProvider {
/// Called once automatically when the app is first launched.
static var appShortcuts: [AppShortcut] {
AppShortcut(
intent: LogWaterIntent(),
phrases: [
"Log water in \(.applicationName)",
"Log a glass of water in \(.applicationName)",
"Track my water with \(.applicationName)"
],
shortTitle: "Log Water",
systemImageName: "drop.fill"
)
}
}
// MARK: - SwiftUI View
struct ContentView: View {
@State private var store = WaterStore.shared
var body: some View {
NavigationStack {
VStack(spacing: 32) {
// Live total
VStack(spacing: 4) {
Text("\(store.totalML)")
.font(.system(size: 72, weight: .bold, design: .rounded))
.contentTransition(.numericText())
.animation(.spring, value: store.totalML)
Text("ml today")
.font(.subheadline)
.foregroundStyle(.secondary)
}
// Quick-log buttons
HStack(spacing: 12) {
ForEach([150, 250, 500], id: \.self) { ml in
Button("\(ml) ml") {
withAnimation { store.log(ml: ml) }
}
.buttonStyle(.bordered)
.accessibilityLabel("Log \(ml) millilitres of water")
}
}
// Invite users to add the Siri shortcut
ShortcutsLink()
.shortcutsLinkStyle(.automaticOutline)
.accessibilityLabel("Add Log Water to Siri")
}
.padding()
.navigationTitle("Hydration")
}
}
}
#Preview {
ContentView()
}
How it works
-
AppIntent conformance.
LogWaterIntentdeclarestitleanddescriptionso the Shortcuts app can display metadata, then implementsperform()— the async method the system calls when the shortcut fires. ReturningProvidesDialogin the return type lets Siri speak a confirmation aloud. -
@Parameter for Siri input. The
@Parameter(title:default:inclusiveRange:)macro lets Siri prompt the user for a value when none is provided. Supplying adefaultmeans the shortcut also works hands-free with no follow-up question. -
AppShortcutsProvider registration.
WaterShortcutsis discovered automatically — you never call it directly. iOS readsappShortcutson first launch and indexes the phrases. The\(.applicationName)interpolation inserts your CFBundleDisplayName, which Siri requires in at least one phrase for voice activation. -
ShortcutsLink view. Placing
ShortcutsLink()renders the system-provided "Add to Siri" button styled to match the current color scheme..shortcutsLinkStyle(.automaticOutline)adapts between light and dark without extra code. -
contentTransition & animation.
.contentTransition(.numericText())on the total label produces a slot-machine count-up effect each time a new amount is logged — a small polish touch that requires iOS 17+.
Variants
Entity-based shortcut (dynamic phrases)
When your shortcuts refer to user-specific content (e.g. a named workout or saved item), conform your model to AppEntity and use @Parameter with an EntityQuery. Siri resolves the spoken name to the correct record at runtime.
struct Drink: AppEntity {
static let typeDisplayRepresentation: TypeDisplayRepresentation = "Drink"
static let defaultQuery = DrinkQuery()
var id: UUID
var name: String
var ml: Int
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(title: "\(name) (\(ml) ml)")
}
}
struct DrinkQuery: EntityQuery {
func entities(for ids: [UUID]) async throws -> [Drink] {
DrinkStore.shared.drinks.filter { ids.contains($0.id) }
}
func suggestedEntities() async throws -> [Drink] {
DrinkStore.shared.drinks
}
}
struct LogDrinkIntent: AppIntent {
static let title: LocalizedStringResource = "Log a Drink"
@Parameter(title: "Drink")
var drink: Drink
func perform() async throws -> some IntentResult & ProvidesDialog {
WaterStore.shared.log(ml: drink.ml)
return .result(dialog: "Logged \(drink.name)!")
}
}
Updating shortcuts at runtime
If your available shortcuts change (e.g. after a user creates a new item), call WaterShortcuts.updateAppShortcutParameters() from your data store after the mutation. This re-indexes phrases without requiring a relaunch. It is a static async method — call it with Task { await WaterShortcuts.updateAppShortcutParameters() } from a synchronous context.
Common pitfalls
-
🛑 iOS 16 vs 17 API surface.
AppShortcutsProviderandAppIntentexist from iOS 16, butShortcutsLinkand.shortcutsLinkStylerequire iOS 17+. Gate behindif #available(iOS 17, *)if you still support iOS 16. -
🛑 Missing \(.applicationName) in phrases. At least one phrase in each
AppShortcutmust contain the\(.applicationName)interpolation. Omitting it causes the shortcut to be silently dropped from Siri voice activation (it still appears in the Shortcuts app). -
🛑 perform() runs off the main actor.
perform()executes on a background thread. Any UIKit/SwiftUI mutations must be dispatched viaawait MainActor.run { … }. Accessing@MainActor-isolated state directly will raise a Swift concurrency warning or crash in strict concurrency mode. -
🛑 Not calling updateAppShortcutParameters(). If your entity list changes (items added, deleted, renamed) and you forget to call
updateAppShortcutParameters(), Siri continues to offer stale entity names and will fail to resolve them at runtime. -
🛑 Accessibility on ShortcutsLink. The system button has a default accessibility label, but it can be unclear without context. Always add
.accessibilityLabel("Add Log Water to Siri")so VoiceOver users understand what action will be added.
Prompt this with Claude Code
When using Soarias or Claude Code directly to implement this:
Implement App Shortcuts in SwiftUI for iOS 17+. Use AppIntents and AppShortcutsProvider. Define at least one parameterised AppIntent with a @Parameter. Register three natural-language Siri phrases, including \(.applicationName). Surface a ShortcutsLink in the main SwiftUI view. Make it accessible (VoiceOver labels on ShortcutsLink and quick-log buttons). Add a #Preview with realistic sample data.
In the Soarias Build phase, paste this prompt into the implementation panel after your screen mockups are approved — Claude Code will scaffold the intent, provider, and view in one pass, leaving you to wire in your real data store.
Related
FAQ
Does this work on iOS 16?
AppIntent and AppShortcutsProvider were introduced in iOS 16, so the intent logic compiles on iOS 16. However, ShortcutsLink and .shortcutsLinkStyle are iOS 17+ only. Gate the button with if #available(iOS 17, *) and offer a fallback such as a deep-link to the Shortcuts app if you need iOS 16 support.
How many Siri phrases can I register per shortcut?
Apple doesn't publish a hard cap, but the practical limit for reliable phrase matching is around ten phrases per AppShortcut. Favour natural sentence variation — subject/verb/object reorderings — over synonyms. Siri handles light paraphrasing automatically, so you don't need an exhaustive list. Keep at least one phrase per locale if you localise your app, and always include \(.applicationName) in one phrase.
What's the UIKit equivalent?
The AppIntents framework is UIKit-agnostic — AppIntent and AppShortcutsProvider work identically in a UIKit project. The only SwiftUI-specific piece is ShortcutsLink; in UIKit, present SiriUISnippetViewController (deprecated) or use a custom button that deep-links to shortcuts://. The intent and provider code is fully shared between UIKit and SwiftUI targets.
Last reviewed: 2026-05-11 by the Soarias team.