I’ve shipped App Intents on three projects. Done right, users get their job done without opening the app, triggered from the Shortcuts app, Siri, Spotlight, and even Focus mode filters. Done wrong, it stays an invisible feature nobody uses.
A basic intent: adding a task
import AppIntents
struct AddTaskIntent: AppIntent {
static var title: LocalizedStringResource = "Add Task"
static var description = IntentDescription("Quickly add a task to the list")
@Parameter(title: "Task") var text: String
func perform() async throws -> some IntentResult & ProvidesDialog {
try await TaskStore.shared.add(text)
return .result(dialog: "Added: (text)")
}
}Even an intent this small shows up in the Shortcuts app and responds to Siri with “AppName, add task X”. No Info.plist entry or storyboard required.
AppShortcutsProvider: the visibility switch
Declaring the intent isn’t enough, the user needs to be able to discover it through AppShortcutsProvider:
struct MyAppShortcuts: AppShortcutsProvider {
static var appShortcuts: [AppShortcut] {
AppShortcut(
intent: AddTaskIntent(),
phrases: [
"Add a task to (.applicationName)",
"Create task in (.applicationName)"
],
shortTitle: "Add Task",
systemImageName: "plus.circle"
)
}
}The phrases are the part that matters. applicationName is a placeholder for your app name, so if the app is called “Notes”, the user says “Add a task to Notes”.
Parametrized intents
If you want priority as a parameter:
enum Priority: String, AppEnum {
case low, medium, high
static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Priority")
static var caseDisplayRepresentations: [Priority: DisplayRepresentation] = [
.low: "Low", .medium: "Medium", .high: "High"
]
}
struct AddTaskIntent: AppIntent {
@Parameter(title: "Task") var text: String
@Parameter(title: "Priority") var priority: Priority
func perform() async throws -> some IntentResult {
try await TaskStore.shared.add(text, priority: priority)
return .result()
}
}The Shortcuts app renders the parameter as a dropdown. On a Siri invocation, missing parameters get asked for.
EntityQuery: working with entities
For a “complete task” intent, you need to let the user pick which task. You do that with AppEntity and EntityQuery:
struct TaskEntity: AppEntity {
static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Task")
static var defaultQuery = TaskQuery()
let id: UUID
let title: String
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(title: "(title)")
}
}
struct TaskQuery: EntityQuery {
func entities(for identifiers: [UUID]) async throws -> [TaskEntity] {
try await TaskStore.shared.fetch(ids: identifiers)
}
func suggestedEntities() async throws -> [TaskEntity] {
try await TaskStore.shared.fetchOpen(limit: 10)
}
}Focus filter: a real use case I built
Only work tasks in the Work focus, personal ones in the Personal focus. With SetFocusFilterIntent, the user sees my app’s filter in the Focus mode settings:
struct WorkFocusFilter: SetFocusFilterIntent {
static var title: LocalizedStringResource = "Work Tasks"
@Parameter(title: "Tag") var tag: String
func perform() async throws -> some IntentResult {
UserDefaults.standard.set(tag, forKey: "focusTag")
NotificationCenter.default.post(name: .focusChanged, object: nil)
return .result()
}
}When the main app wakes up, it reads focusTag and filters the list accordingly.
Wiring into interactive widgets
In iOS 17 widgets, a button or toggle can fire an App Intent directly. On my todo widget, when the checkbox gets tapped:
Button(intent: ToggleTaskIntent(taskId: task.id)) {
Image(systemName: task.isDone ? "checkmark.circle.fill" : "circle")
}The widget refresh happens automatically, no manual timeline reload.
Pitfall: returning a dialog from async
If you want to show a dialog after an async operation, conform to ProvidesDialog and return .result(dialog:). If you only return .result(), Siri stays silent and the user doesn’t know what happened.
Localization
Wherever you use LocalizedStringResource, the String Catalog (.xcstrings) integration is automatic. Be careful with interpolation parameters in phrases though; some languages have grammar rules that break the English fixed-template assumption.
Final advice
Three good intents beat thirty half-done ones. If users can reach three fast actions from your app, that alone is enough to make it part of their daily flow. The features people actually keep using come out of that kind of restraint.