```html SwiftUI: How to Implement App Shortcuts (iOS 17+, 2026)

How to Implement App Shortcuts in SwiftUI

iOS 17+ Xcode 16+ Intermediate APIs: AppIntents / AppShortcutsProvider Updated: May 11, 2026
TL;DR

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

  1. AppIntent conformance. LogWaterIntent declares title and description so the Shortcuts app can display metadata, then implements perform() — the async method the system calls when the shortcut fires. Returning ProvidesDialog in the return type lets Siri speak a confirmation aloud.
  2. @Parameter for Siri input. The @Parameter(title:default:inclusiveRange:) macro lets Siri prompt the user for a value when none is provided. Supplying a default means the shortcut also works hands-free with no follow-up question.
  3. AppShortcutsProvider registration. WaterShortcuts is discovered automatically — you never call it directly. iOS reads appShortcuts on first launch and indexes the phrases. The \(.applicationName) interpolation inserts your CFBundleDisplayName, which Siri requires in at least one phrase for voice activation.
  4. 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.
  5. 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

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.

```