app-intents
App Intents Framework
Comprehensive guide to App Intents for Siri integration, Shortcuts, Spotlight, Action Button, and interactive snippets in iOS 26.
Prerequisites
- iOS 16+ for App Intents (iOS 26 recommended)
- Xcode 26+
Framework Overview
Core Concepts
- Intents = Verbs (actions your app can perform)
- App Entities = Dynamic Nouns (content in your app)
- App Enums = Static Nouns (fixed options)
Where App Intents Appear
- Siri - Voice-activated commands
- Shortcuts - User-created automations
- Spotlight - Search suggestions and actions
- Action Button - iPhone 15 Pro hardware button
- Apple Pencil - Squeeze gesture
- Focus Filters - Customize app behavior per focus
Import
import AppIntents
Creating App Intents
Basic Intent
import AppIntents
struct OpenNoteIntent: AppIntent {
static var title: LocalizedStringResource = "Open Note"
static var description = IntentDescription("Opens a specific note in the app")
@Parameter(title: "Note")
var note: NoteEntity
func perform() async throws -> some IntentResult {
// Open the note in your app
await NoteManager.shared.open(note.id)
return .result()
}
}
Intent with Parameters
struct CreateNoteIntent: AppIntent {
static var title: LocalizedStringResource = "Create Note"
static var description = IntentDescription("Creates a new note with the specified content")
@Parameter(title: "Title")
var title: String
@Parameter(title: "Content", default: "")
var content: String
@Parameter(title: "Folder", optionsProvider: FolderOptionsProvider())
var folder: FolderEntity?
func perform() async throws -> some IntentResult {
let note = await NoteManager.shared.create(
title: title,
content: content,
in: folder?.id
)
return .result(value: NoteEntity(note: note))
}
struct FolderOptionsProvider: DynamicOptionsProvider {
func results() async throws -> [FolderEntity] {
let folders = await FolderManager.shared.all()
return folders.map { FolderEntity(folder: $0) }
}
}
}
Intent Results
// Simple result
return .result()
// Result with value
return .result(value: noteEntity)
// Result with dialog (for Siri)
return .result(dialog: "Note created successfully")
// Result with view snippet
return .result(
dialog: "Here's your note",
view: NoteSnippetView(note: note)
)
// Result opening app
return .result(opensIntent: OpenNoteIntent(note: noteEntity))
App Entities
Defining an Entity
import AppIntents
struct NoteEntity: AppEntity {
static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Note")
var id: UUID
var title: String
var content: String
var createdAt: Date
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(
title: "\(title)",
subtitle: "\(content.prefix(50))...",
image: .init(systemName: "doc.text")
)
}
static var defaultQuery = NoteEntityQuery()
init(note: Note) {
self.id = note.id
self.title = note.title
self.content = note.content
self.createdAt = note.createdAt
}
}
Entity Query
struct NoteEntityQuery: EntityQuery {
func entities(for identifiers: [UUID]) async throws -> [NoteEntity] {
let notes = await NoteManager.shared.fetch(ids: identifiers)
return notes.map { NoteEntity(note: $0) }
}
func suggestedEntities() async throws -> [NoteEntity] {
let recentNotes = await NoteManager.shared.recentNotes(limit: 5)
return recentNotes.map { NoteEntity(note: $0) }
}
}
Searchable Entity Query
struct NoteEntityQuery: EntityStringQuery {
func entities(for identifiers: [UUID]) async throws -> [NoteEntity] {
let notes = await NoteManager.shared.fetch(ids: identifiers)
return notes.map { NoteEntity(note: $0) }
}
func entities(matching string: String) async throws -> [NoteEntity] {
let notes = await NoteManager.shared.search(query: string)
return notes.map { NoteEntity(note: $0) }
}
func suggestedEntities() async throws -> [NoteEntity] {
let recentNotes = await NoteManager.shared.recentNotes(limit: 5)
return recentNotes.map { NoteEntity(note: $0) }
}
}
Property Queries (iOS 17+)
struct NoteEntityQuery: EntityPropertyQuery {
static var properties = QueryProperties {
Property(\NoteEntity.$title) {
EqualToComparator { $0 }
ContainsComparator { $0 }
}
Property(\NoteEntity.$createdAt) {
LessThanComparator { $0 }
GreaterThanComparator { $0 }
}
}
static var sortingOptions = SortingOptions {
SortableBy(\NoteEntity.$title)
SortableBy(\NoteEntity.$createdAt)
}
func entities(
matching comparators: [EntityQueryComparator<NoteEntity>],
mode: ComparatorMode,
sortedBy: [EntityQuerySort<NoteEntity>],
limit: Int?
) async throws -> [NoteEntity] {
// Apply filters and sorting
var notes = await NoteManager.shared.all()
// Apply comparators
for comparator in comparators {
notes = notes.filter { comparator.evaluate($0) }
}
// Apply sorting
for sort in sortedBy {
notes.sort(by: sort.compare)
}
// Apply limit
if let limit {
notes = Array(notes.prefix(limit))
}
return notes.map { NoteEntity(note: $0) }
}
}
App Enums
Defining Enums
import AppIntents
enum NoteCategory: String, AppEnum {
case personal
case work
case ideas
case todo
static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Category")
static var caseDisplayRepresentations: [NoteCategory: DisplayRepresentation] = [
.personal: DisplayRepresentation(title: "Personal", image: .init(systemName: "person")),
.work: DisplayRepresentation(title: "Work", image: .init(systemName: "briefcase")),
.ideas: DisplayRepresentation(title: "Ideas", image: .init(systemName: "lightbulb")),
.todo: DisplayRepresentation(title: "To-Do", image: .init(systemName: "checklist"))
]
}
Using Enums in Intents
struct FilterNotesIntent: AppIntent {
static var title: LocalizedStringResource = "Filter Notes"
@Parameter(title: "Category")
var category: NoteCategory
func perform() async throws -> some IntentResult {
let notes = await NoteManager.shared.filter(by: category)
return .result(value: notes.map { NoteEntity(note: $0) })
}
}
App Shortcuts
AppShortcutsProvider
import AppIntents
struct MyAppShortcuts: AppShortcutsProvider {
static var appShortcuts: [AppShortcut] {
AppShortcut(
intent: CreateNoteIntent(),
phrases: [
"Create a note in \(.applicationName)",
"New note in \(.applicationName)",
"Add note to \(.applicationName)"
],
shortTitle: "Create Note",
systemImageName: "plus.circle"
)
AppShortcut(
intent: OpenRecentNoteIntent(),
phrases: [
"Open my recent note in \(.applicationName)",
"Show last note in \(.applicationName)"
],
shortTitle: "Recent Note",
systemImageName: "clock"
)
AppShortcut(
intent: SearchNotesIntent(),
phrases: [
"Search notes in \(.applicationName)",
"Find \(\.$query) in \(.applicationName)"
],
shortTitle: "Search",
systemImageName: "magnifyingglass"
)
}
}
Phrase Rules
- Must include
\(.applicationName)placeholder - Maximum one parameter reference per phrase
- Parameter must use
\(\.$parameterName)syntax - Keep phrases natural and varied
Automatic Registration
App Shortcuts are automatically registered when:
- App is installed
- App is updated
- AppShortcutsProvider is modified
Interactive Snippets (MicroUI)
Result Snippets
struct ShowNoteIntent: AppIntent {
static var title: LocalizedStringResource = "Show Note"
@Parameter(title: "Note")
var note: NoteEntity
func perform() async throws -> some IntentResult & ShowsSnippetView {
return .result(
dialog: "Here's your note",
view: NoteSnippetView(note: note)
)
}
}
struct NoteSnippetView: View {
let note: NoteEntity
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text(note.title)
.font(.headline)
Text(note.content)
.font(.body)
.lineLimit(3)
Text(note.createdAt, style: .relative)
.font(.caption)
.foregroundStyle(.secondary)
}
.padding()
}
}
Confirmation Snippets
struct DeleteNoteIntent: AppIntent {
static var title: LocalizedStringResource = "Delete Note"
@Parameter(title: "Note")
var note: NoteEntity
func perform() async throws -> some IntentResult {
try await requestConfirmation(
result: .result(
dialog: "Are you sure you want to delete this note?",
view: DeleteConfirmationView(note: note)
)
)
await NoteManager.shared.delete(note.id)
return .result(dialog: "Note deleted")
}
}
struct DeleteConfirmationView: View {
let note: NoteEntity
var body: some View {
VStack(spacing: 12) {
Image(systemName: "trash")
.font(.largeTitle)
.foregroundStyle(.red)
Text("Delete '\(note.title)'?")
.font(.headline)
Text("This action cannot be undone.")
.font(.caption)
.foregroundStyle(.secondary)
}
.padding()
}
}
Interactive Snippet Buttons
struct NoteActionsSnippetView: View {
let note: NoteEntity
var body: some View {
VStack(spacing: 12) {
Text(note.title)
.font(.headline)
HStack(spacing: 16) {
Button(intent: EditNoteIntent(note: note)) {
Label("Edit", systemImage: "pencil")
}
Button(intent: ShareNoteIntent(note: note)) {
Label("Share", systemImage: "square.and.arrow.up")
}
}
.buttonStyle(.bordered)
}
.padding()
}
}
Snippet Design Guidelines
- Keep snippets compact (fits in Siri/Spotlight card)
- Use larger text for readability
- High contrast colors
- Clear, tappable buttons
- Concise content
Foreground vs Background Execution
Background Execution (Default)
struct QuickNoteIntent: AppIntent {
static var title: LocalizedStringResource = "Quick Note"
@Parameter(title: "Content")
var content: String
// Runs in background without opening app
func perform() async throws -> some IntentResult {
await NoteManager.shared.quickCreate(content: content)
return .result(dialog: "Note saved!")
}
}
Foreground Execution
struct EditNoteIntent: AppIntent {
static var title: LocalizedStringResource = "Edit Note"
// Opens app when intent runs
static var openAppWhenRun = true
@Parameter(title: "Note")
var note: NoteEntity
func perform() async throws -> some IntentResult {
// App is now in foreground
await NoteManager.shared.openEditor(for: note.id)
return .result()
}
}
Conditional Foreground
struct ViewNoteIntent: AppIntent {
static var title: LocalizedStringResource = "View Note"
@Parameter(title: "Note")
var note: NoteEntity
@Parameter(title: "Open in App")
var openInApp: Bool
static var openAppWhenRun: Bool {
// Dynamically determined
return false
}
func perform() async throws -> some IntentResult {
if openInApp {
// Return result that opens app
return .result(opensIntent: OpenNoteIntent(note: note))
} else {
// Return snippet view
return .result(view: NoteSnippetView(note: note))
}
}
}
Dependency Injection
@Dependency Property Wrapper
struct CreateNoteIntent: AppIntent {
static var title: LocalizedStringResource = "Create Note"
@Dependency
var noteService: NoteService
@Parameter(title: "Title")
var title: String
func perform() async throws -> some IntentResult {
let note = try await noteService.create(title: title)
return .result(value: NoteEntity(note: note))
}
}
Registering Dependencies
Register dependencies early in app lifecycle:
@main
struct MyApp: App {
init() {
// Register dependencies for App Intents
AppDependencyManager.shared.add(dependency: NoteService.shared)
AppDependencyManager.shared.add(dependency: FolderService.shared)
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Focus Filters
Defining a Focus Filter
import AppIntents
struct NoteFocusFilter: SetFocusFilterIntent {
static var title: LocalizedStringResource = "Set Note Filter"
static var description = IntentDescription("Filter notes during this Focus")
@Parameter(title: "Show Categories")
var categories: [NoteCategory]?
@Parameter(title: "Hide Work Notes")
var hideWork: Bool?
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(title: "Note Filter")
}
func perform() async throws -> some IntentResult {
// Apply filter settings
if let categories {
FocusState.shared.visibleCategories = categories
}
if let hideWork {
FocusState.shared.hideWorkNotes = hideWork
}
return .result()
}
}
Spotlight Integration
Featured in Spotlight
Intents automatically appear in Spotlight when:
- User searches for related terms
- App Shortcuts are defined
- Entities match search queries
Donating Activities
import Intents
func userViewedNote(_ note: Note) {
let activity = NSUserActivity(activityType: "com.app.viewNote")
activity.title = note.title
activity.userInfo = ["noteId": note.id.uuidString]
activity.isEligibleForSearch = true
activity.isEligibleForPrediction = true
// Associate with App Intent
activity.shortcutAvailability = .sleepInBed
UIApplication.shared.currentUserActivity = activity
}
Action Button & Apple Pencil
Action Button Intent
struct QuickCaptureIntent: AppIntent {
static var title: LocalizedStringResource = "Quick Capture"
static var description = IntentDescription("Quickly capture a thought")
// Good for Action Button - fast execution
func perform() async throws -> some IntentResult & OpensIntent {
// Create new capture and open editor
let capture = await CaptureManager.shared.createQuick()
return .result(opensIntent: OpenCaptureIntent(capture: capture))
}
}
Users configure Action Button in Settings → Action Button → Shortcut → [Your App Shortcut]
Apple Pencil Squeeze
Same intents work for Apple Pencil squeeze gesture on supported devices.
Testing Intents
Testing in Shortcuts App
- Build and run your app
- Open Shortcuts app
- Create new shortcut
- Search for your app's intents
- Configure parameters
- Run shortcut
Testing with Siri
- Build and run app
- Say: "Hey Siri, [your phrase]"
- Verify Siri understands and executes
Programmatic Testing
import Testing
import AppIntents
@Test
func testCreateNoteIntent() async throws {
var intent = CreateNoteIntent()
intent.title = "Test Note"
intent.content = "Test content"
let result = try await intent.perform()
// Verify result
#expect(result != nil)
}
Best Practices
1. Natural Phrases
// GOOD: Natural language
"Create a note in \(.applicationName)"
"Add \(\.$title) to my notes"
// AVOID: Technical language
"Execute CreateNote command in \(.applicationName)"
2. Meaningful Dialogs
// GOOD: Contextual confirmation
return .result(dialog: "Created note '\(title)' in your Ideas folder")
// AVOID: Generic responses
return .result(dialog: "Done")
3. Fast Background Execution
// Keep background intents fast
func perform() async throws -> some IntentResult {
// Quick operation
await quickSave(data)
return .result(dialog: "Saved!")
}
4. Graceful Error Handling
func perform() async throws -> some IntentResult {
guard let note = await NoteManager.shared.find(id: noteId) else {
throw IntentError.noteNotFound
}
// Continue...
}
enum IntentError: Error, CustomLocalizedStringResourceConvertible {
case noteNotFound
var localizedStringResource: LocalizedStringResource {
switch self {
case .noteNotFound:
return "Note not found. It may have been deleted."
}
}
}
Official Resources
More from bluewaves-creations/bluewaves-skills
photographer-testino
Generate images in Mario Testino's glamorous vibrant style. Use when users ask for Testino style, high fashion glamour, bold saturated colors, warm luxurious photography, dynamic sensual energy.
35photographer-lindbergh
Generate images in Peter Lindbergh's iconic black and white style. Use when users ask for Lindbergh style, raw authentic beauty, emotional B&W portraits, supermodel aesthetic, or unretouched natural photography.
30photographer-lachapelle
Generate images in David LaChapelle's surreal pop art style. Use when users ask for LaChapelle style, pop surrealism, hyper-saturated colors, theatrical staging, baroque maximalism, kitsch aesthetic.
24epub-creator
Create production-quality EPUB 3 ebooks from markdown and images with automated QA, formatting fixes, and validation. Use when creating ebooks, converting markdown to EPUB, or compiling chapters into a publishable book. Handles markdown quirks, generates TOC, adds covers, and validates output automatically.
22photographer-vonunwerth
Generate images in Ellen von Unwerth's playful vintage style. Use when users ask for von Unwerth style, playful sensuality, vintage film noir, whimsical feminine photography, retro glamour, narrative storytelling.
19photographer-ritts
Generate images in Herb Ritts' sculptural black and white style. Use when users ask for Ritts style, classical Greek aesthetic, sculptural body photography, California golden hour, minimalist athletic portraits.
18