How to Implement Siri Shortcuts in SwiftUI
Conform a struct to AppIntent, implement perform(), then register phrase triggers in an AppShortcutsProvider. Drop a ShortcutsLink anywhere in your SwiftUI view so users can add it to the Shortcuts app in one tap.
import AppIntents
struct LogWaterIntent: AppIntent {
static let title: LocalizedStringResource = "Log Water"
@Parameter(title: "Ounces", default: 8.0)
var amount: Double
func perform() async throws -> some IntentResult & ProvidesDialog {
WaterStore.shared.log(ounces: amount)
return .result(dialog: "Logged \(amount) oz. Stay hydrated!")
}
}
// In your SwiftUI view:
ShortcutsLink()
.shortcutsLinkStyle(.automaticOutline)
Full implementation
The modern AppIntents framework (iOS 16+, greatly improved in iOS 17) replaces the older SiriKit INIntent approach with pure Swift. You define intents as structs, expose them to Siri via AppShortcutsProvider — no Info.plist intent declarations needed — and surface a one-tap entry point with ShortcutsLink. iOS picks up your shortcut phrases automatically when the user first launches your app.
import AppIntents
import SwiftUI
// MARK: - 1. Define the Intent
struct LogWaterIntent: AppIntent {
static let title: LocalizedStringResource = "Log Water Intake"
static let description = IntentDescription(
"Log your daily water intake directly from Siri or the Shortcuts app."
)
// Set false to run silently in the background
static var openAppWhenRun: Bool = false
@Parameter(title: "Amount (oz)", description: "Ounces of water to log.", default: 8.0)
var amount: Double
func perform() async throws -> some IntentResult & ReturnsValue<Double> & ProvidesDialog {
try await WaterStore.shared.log(ounces: amount)
let total = await WaterStore.shared.todayTotal()
return .result(
value: amount,
dialog: "Logged \(amount) oz. You've had \(total) oz today — great work!"
)
}
}
// MARK: - 2. Register Phrases with AppShortcutsProvider
struct WaterShortcutsProvider: AppShortcutsProvider {
/// Phrases must include \(.applicationName) at least once.
static var appShortcuts: [AppShortcut] {
AppShortcut(
intent: LogWaterIntent(),
phrases: [
"Log water in \(.applicationName)",
"Track my water intake in \(.applicationName)",
"I drank water in \(.applicationName)"
],
shortTitle: "Log Water",
systemImageName: "drop.fill"
)
}
// Called by the system when shortcuts need to be updated
static var shortcutTileColor: ShortcutTileColor = .blue
}
// MARK: - 3. Persistent Store (actor-isolated for Swift concurrency)
actor WaterStore {
static let shared = WaterStore()
private let defaults = UserDefaults.standard
private var todayKey: String {
let fmt = Date.FormatStyle().month(.twoDigits).day(.twoDigits).year()
return "waterLog_\(Date().formatted(fmt))"
}
func log(ounces: Double) throws {
let current = defaults.double(forKey: todayKey)
defaults.set(current + ounces, forKey: todayKey)
}
func todayTotal() -> Double {
defaults.double(forKey: todayKey)
}
}
// MARK: - 4. SwiftUI View with ShortcutsLink
struct ShortcutsOnboardingView: View {
@State private var todayTotal: Double = 0
var body: some View {
VStack(spacing: 24) {
Image(systemName: "drop.fill")
.font(.system(size: 56))
.foregroundStyle(.blue)
Text("Siri Shortcuts")
.font(.largeTitle.bold())
Text("Ask Siri \"Log water in \(Bundle.main.displayName)\" or add it to the Shortcuts app below.")
.multilineTextAlignment(.center)
.foregroundStyle(.secondary)
Text("Today: \(todayTotal, format: .number) oz")
.font(.headline)
.foregroundStyle(.blue)
// ShortcutsLink opens the system sheet to add/manage your shortcuts
ShortcutsLink()
.shortcutsLinkStyle(.automaticOutline)
.accessibilityLabel("Add Siri Shortcuts")
}
.padding(32)
.task {
todayTotal = await WaterStore.shared.todayTotal()
}
}
}
// MARK: - Bundle helper
extension Bundle {
var displayName: String {
object(forInfoDictionaryKey: "CFBundleDisplayName") as? String
?? object(forInfoDictionaryKey: "CFBundleName") as? String
?? "App"
}
}
#Preview {
ShortcutsOnboardingView()
}
How it works
-
AppIntentconformance —LogWaterIntentis a plain Swift struct. Thetitlestatic property is what Siri and the Shortcuts app display. SettingopenAppWhenRun = falselets the intent run entirely in the background; set it totrueif you need to deep-link into your UI. -
@Parameterproperty wrapper — Each parameter marked with@Parameterbecomes a slot Siri can fill from the spoken phrase or prompt the user for. Thedefault:value is used if the user doesn't specify one, enabling "Hey Siri, log water in MyApp" without any follow-up question. -
perform()return types — The return typesome IntentResult & ReturnsValue<Double> & ProvidesDialogtells the system this intent gives back a typed value (usable in Shortcuts automation chains) and a spoken/displayed response viaProvidesDialog. You can mix and match these protocols as needed. -
AppShortcutsProvider— TheWaterShortcutsProviderstruct registers the shortcut automatically the first time the app launches — no user setup required. Phrases must embed\(.applicationName)so Siri can disambiguate between multiple apps. -
ShortcutsLink— This built-in SwiftUI view presents the system sheet where users can add, rename, or delete your shortcuts. The.shortcutsLinkStyle(.automaticOutline)adapts to light/dark mode automatically.
Variants
Intent with an enum parameter (e.g. drink type)
enum DrinkType: String, AppEnum {
case water, coffee, tea, juice
static let typeDisplayRepresentation: TypeDisplayRepresentation = "Drink Type"
static let caseDisplayRepresentations: [DrinkType: DisplayRepresentation] = [
.water: "Water",
.coffee: "Coffee",
.tea: "Tea",
.juice: "Juice"
]
}
struct LogDrinkIntent: AppIntent {
static let title: LocalizedStringResource = "Log a Drink"
static var openAppWhenRun: Bool = false
@Parameter(title: "Drink Type", default: .water)
var drink: DrinkType
@Parameter(title: "Amount (oz)", default: 8.0)
var amount: Double
func perform() async throws -> some IntentResult & ProvidesDialog {
// persist…
return .result(dialog: "Logged \(amount) oz of \(drink.rawValue).")
}
}
Donating a shortcut after in-app action (iOS 17+)
You can proactively surface your shortcut in Siri Suggestions by donating it at the moment a user performs an action in-app. Call AppShortcutsProvider.updateAppShortcutParameters() after saving new data so the system refreshes the intent's suggested phrases and parameters. In SwiftData or Observable model layers, trigger this in the .task or onChange modifier right after a successful write.
Common pitfalls
-
🚫 Missing
\(.applicationName)in phrases. Every phrase in yourAppShortcutmust contain\(.applicationName)— if you omit it, the system silently discards the phrase and Siri won't recognize it. This is a compile-time string interpolation requirement unique to AppIntents, not a regular Swift string. -
🚫 Forgetting
AppShortcutsProvideron first launch. UnlikeINInteraction.donate(), you don't callAppShortcutsProvidermanually — iOS calls it automatically. But if you don't instantiate any conforming type before the user tries Siri, phrases won't be registered. Add aWaterShortcutsProvider.updateAppShortcutParameters()call in your@main App.init()to be safe. -
🚫 Running blocking work in
perform().perform()isasync throws— alwaysawaityour database writes and never callURLSessionsynchronously. Intents have a tight execution budget (~10 s for background, less for voice-activated); heavy work should be offloaded before returning. -
🚫 Accessibility on
ShortcutsLink. The defaultShortcutsLinkhas no customaccessibilityLabel. Always add one so VoiceOver users hear something meaningful instead of a generic button announcement.
Prompt this with Claude Code
When using Soarias or Claude Code directly to implement this:
Implement Siri Shortcuts in SwiftUI for iOS 17+. Use AppIntents: AppIntent, @Parameter, AppShortcutsProvider, and ShortcutsLink. The intent should run in the background (openAppWhenRun = false). Make it accessible (VoiceOver labels on ShortcutsLink). Add a #Preview with realistic sample data.
In Soarias's Build phase, paste this into the prompt bar after your screens are scaffolded — Claude Code will wire the AppIntent into your existing SwiftData model and drop the ShortcutsLink into the correct settings or onboarding view automatically.
Related
FAQ
Does this work on iOS 16?
The AppIntents framework was introduced in iOS 16, but AppShortcutsProvider and ShortcutsLink both arrived with iOS 16.0 as well. However, iOS 17 added significant improvements — including richer entity queries, the updated @Parameter dialog system, and more stable phrase registration. This guide targets iOS 17+ to use those improvements. If you need iOS 16 support, the same code compiles but phrase auto-registration is less reliable; compensate by calling AppShortcutsProvider.updateAppShortcutParameters() on every cold launch.
Can I return rich UI (e.g. a custom snippet) from an AppIntent?
Yes — conform your return type to ShowsSnippetView and return a SwiftUI view from perform(). This renders a compact widget-like card inside the Shortcuts app and in Siri's response overlay. The view must be lightweight (no navigation, no state) since it runs in an extension process. Use the same AppIntentResult builder — just add & ShowsSnippetView to the return type and implement var body: some View directly on the result struct.
What's the UIKit / SiriKit equivalent?
The legacy approach uses INIntent / INExtension with an Intents Extension target and an IntentsUI Extension for custom UI. You had to declare intent classes in Info.plist and handle donation with INInteraction.donate(). AppIntents fully replaces this — Apple deprecated the INIntent-based Siri Shortcuts flow in favor of AppIntents starting iOS 16. For new apps, always use AppIntents; migration from INIntent to AppIntents is mechanical and well worth doing.
Last reviewed: 2026-05-11 by the Soarias team.