
App Intents
- 3 installs
- 591 repo stars
- Updated July 24, 2026
- rshankras/claude-code-apple-skills
Builds App Intents that expose app functionality to Siri, Shortcuts, Spotlight, and Apple Intelligence, including intent modes, interactive snippets, and entity indexing.
About
Guides implementation of the App Intents framework from basic actions through interactive snippets, visual intelligence, and Spotlight entity indexing. A developer uses it to add Siri, Shortcuts, or Spotlight integration to an iOS/macOS app.
- Decision tree from basic AppIntent to App Shortcuts and advanced features
- Covers IndexedEntity, interactive snippets, and visual intelligence
App Intents by the numbers
- 3 all-time installs (skills.sh)
- +1 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #887 of 1,039 Mobile Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/rshankras/claude-code-apple-skills --skill app-intentsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 3 |
|---|---|
| repo stars | ★ 591 |
| Last updated | July 24, 2026 |
| Repository | rshankras/claude-code-apple-skills ↗ |
What it does
Builds App Intents that expose app functionality to Siri, Shortcuts, Spotlight, and Apple Intelligence, including intent modes, interactive snippets, and entity indexing.
Files
App Intents
Build intents that expose your app's functionality to Siri, Shortcuts, Spotlight, and Apple Intelligence. Covers the full App Intents framework from basic actions through advanced features like interactive snippets, intent modes, visual intelligence integration, and Spotlight entity indexing.
When This Skill Activates
- User wants to add Siri or Shortcuts integration
- User asks about App Intents, AppIntent, or AppEntity
- User needs Spotlight indexing for app content
- User wants to create App Shortcuts with voice phrases
- User is implementing interactive snippets for Siri results
- User asks about intent modes (foreground, background)
- User needs visual intelligence integration via App Intents
- User wants onscreen entity support for Siri/ChatGPT
- User asks about Swift package support for App Intents
Decision Tree
What do you need?
|
+-- Expose an action to Siri/Shortcuts
| +-- Simple action, no UI needed
| | --> Basic AppIntent (intents-basics.md)
| +-- Needs to show UI or ask user questions
| | --> Intent Modes + Interactive Snippets (advanced-features.md)
| +-- Needs a predictable voice phrase
| --> App Shortcuts (intents-basics.md)
|
+-- Make content searchable
| +-- In Spotlight
| | --> IndexedEntity + @Property (entities-spotlight.md)
| +-- In Visual Intelligence
| | --> IntentValueQuery + SemanticContentDescriptor (advanced-features.md)
| +-- As onscreen entities for Siri/ChatGPT
| --> .userActivity() + EntityIdentifier (advanced-features.md)
|
+-- Show rich results in Siri
| +-- Static display only
| | --> .result(view:) snippet (advanced-features.md)
| +-- Interactive buttons/controls
| --> SnippetIntent protocol (advanced-features.md)
|
+-- Present choices to the user
| --> requestChoice(between:) (advanced-features.md)
|
+-- Share intents via Swift Package
--> AppIntentsPackage protocol (advanced-features.md)API Availability
| Feature | Minimum OS | Framework |
|---|---|---|
AppIntent protocol | iOS 16 / macOS 13 | AppIntents |
AppEntity protocol | iOS 16 / macOS 13 | AppIntents |
AppShortcutsProvider | iOS 16 / macOS 13 | AppIntents |
@Parameter macro | iOS 16 / macOS 13 | AppIntents |
IndexedEntity protocol | iOS 18 / macOS 15 | AppIntents |
@Property with indexingKey | iOS 18 / macOS 15 | AppIntents |
Intent Modes (supportedModes) | iOS 26 / macOS 26 | AppIntents |
requestChoice(between:) | iOS 26 / macOS 26 | AppIntents |
@ComputedProperty | iOS 26 / macOS 26 | AppIntents |
@DeferredProperty | iOS 26 / macOS 26 | AppIntents |
SnippetIntent protocol | iOS 26 / macOS 26 | AppIntents |
AppIntentsPackage protocol | iOS 26 / macOS 26 | AppIntents |
Onscreen entities (.userActivity()) | iOS 26 / macOS 26 | AppIntents |
@UnionValue | iOS 18 / macOS 15 | AppIntents |
Quick Reference
| Task | Type/API | Reference File |
|---|---|---|
| Define an action | AppIntent protocol | intents-basics.md |
| Accept parameters | @Parameter macro | intents-basics.md |
| Create voice phrases | AppShortcutsProvider | intents-basics.md |
| Define a data entity | AppEntity protocol | entities-spotlight.md |
| Index in Spotlight | IndexedEntity protocol | entities-spotlight.md |
| Mark indexable fields | @Property(indexingKey:) | entities-spotlight.md |
| Run in background/foreground | supportedModes | advanced-features.md |
| Continue in foreground | continueInForeground() | advanced-features.md |
| Show result UI | .result(view:) | advanced-features.md |
| Interactive result UI | SnippetIntent protocol | advanced-features.md |
| Present choices | requestChoice(between:) | advanced-features.md |
| Visual intelligence search | IntentValueQuery | advanced-features.md |
| Onscreen entity association | .userActivity() modifier | advanced-features.md |
| Computed/deferred properties | @ComputedProperty, @DeferredProperty | advanced-features.md |
| Share via packages | AppIntentsPackage | advanced-features.md |
Process
1. Identify Integration Needs
Read the user's code or requirements to determine:
- What actions should be exposed to Siri/Shortcuts
- What content should be searchable in Spotlight
- Whether interactive snippets are needed for Siri results
- Whether the intent needs foreground UI or can run in background
- Target platform and minimum OS version
2. Load Relevant Reference Files
Based on the need, read from this directory:
- intents-basics.md -- AppIntent protocol, @Parameter, perform(), App Shortcuts
- entities-spotlight.md -- AppEntity, IndexedEntity, Spotlight indexing, @Property
- advanced-features.md -- Intent modes, interactive snippets, visual intelligence, onscreen entities, choices, packages
3. Review or Implement
Apply patterns from the reference files. Check for common mistakes (see Top Mistakes below).
4. Cross-Reference
- For Visual Intelligence camera search, see
apple-intelligence/visual-intelligence/ - For Foundation Models on-device LLM, see
apple-intelligence/foundation-models/ - For deep linking from intents, see
generators/deep-linking/skill
Top Mistakes
These are the most frequent errors when implementing App Intents.
1. Missing static metadata
// ❌ Wrong -- no title or description
struct MyIntent: AppIntent {
func perform() async throws -> some IntentResult {
return .result()
}
}
// ✅ Correct -- static title is required
struct MyIntent: AppIntent {
static var title: LocalizedStringResource = "Do Something"
static var description: IntentDescription = "Performs the action"
func perform() async throws -> some IntentResult {
return .result()
}
}2. Forgetting to index entities after changes
// ❌ Wrong -- entities updated but Spotlight not notified
func saveRecipe(_ recipe: Recipe) {
database.save(recipe)
}
// ✅ Correct -- reindex after mutations
func saveRecipe(_ recipe: Recipe) async throws {
database.save(recipe)
try await CSSearchableIndex.default().indexAppEntities()
}3. Using foreground intent for background-safe work
// ❌ Wrong -- forces app to foreground for a simple toggle
struct ToggleFavoriteIntent: AppIntent {
static var title: LocalizedStringResource = "Toggle Favorite"
static var openAppWhenRun = true // Unnecessary
func perform() async throws -> some IntentResult {
toggleFavorite()
return .result()
}
}
// ✅ Correct -- runs silently in background
struct ToggleFavoriteIntent: AppIntent {
static var title: LocalizedStringResource = "Toggle Favorite"
var supportedModes: IntentModes { .background }
func perform() async throws -> some IntentResult {
toggleFavorite()
return .result()
}
}4. Not providing EntityStringQuery for entities
// ❌ Wrong -- entity has no way to be queried
struct NoteEntity: AppEntity {
var id: String
var title: String
// Missing: static var defaultQuery
}
// ✅ Correct -- provides a query so Siri can resolve entities
struct NoteEntity: AppEntity {
var id: String
var title: String
static var defaultQuery = NoteEntityQuery()
// ... typeDisplayRepresentation, displayRepresentation
}5. Returning too many Spotlight results
// ❌ Wrong -- indexing thousands of items at once blocks the main thread
func indexAll() async throws {
let allItems = database.fetchAll() // 50,000 items
try await CSSearchableIndex.default().indexAppEntities()
}
// ✅ Correct -- batch index and run off main thread
func indexAll() async throws {
try await CSSearchableIndex.default().indexAppEntities(
of: RecipeEntity.self
)
}Review Checklist
Before shipping App Intents integration:
- [ ] Every
AppIntenthas astatic var titleandstatic var description - [ ] Every
AppEntityhastypeDisplayRepresentation,displayRepresentation, anddefaultQuery - [ ]
@Parameterproperties have descriptive titles - [ ] Entities used in Shortcuts have
EntityStringQueryorEntityPropertyQuery - [ ]
IndexedEntitytypes callCSSearchableIndex.default().indexAppEntities()after data changes - [ ]
@Propertyfields used in indexing haveindexingKeyset - [ ] App Shortcuts have clear, natural-language phrases with
\(.applicationName) - [ ] Intent modes match the actual work: background for data ops, foreground for UI
- [ ] Interactive snippets use
SnippetIntent(not plainAppIntent) - [ ]
perform()handles errors gracefully and returns meaningful dialog - [ ] Intents are tested in Shortcuts app and via Siri voice
- [ ] Deep links from entity results navigate to the correct screen
References
- App Intents framework
- Making your app's functionality available to Siri
- App Shortcuts
- IndexedEntity
- Spotlight integration
/Users/ravishankar/Downloads/docs/AppIntents-Updates.md
Advanced Features
Intent modes, interactive snippets, visual intelligence integration, onscreen entities, multiple choice, property macros, and Swift package support. These features require iOS 26 / macOS 26 unless noted otherwise.
Intent Modes
Control whether your intent runs in the background or needs to come to the foreground. Available in iOS 26 / macOS 26.
supportedModes Property
struct SyncDataIntent: AppIntent {
static var title: LocalizedStringResource = "Sync Data"
// Runs entirely in the background -- no UI needed
var supportedModes: IntentModes { .background }
func perform() async throws -> some IntentResult & ProvidesDialog {
try await DataService.shared.syncAll()
return .result(dialog: "Data synced successfully.")
}
}Mode Options
| Mode | Behavior |
|---|---|
.background | Runs without showing the app. Best for data operations. |
.foreground(.immediate) | Opens the app immediately before perform() runs. |
.foreground(.dynamic) | Starts in background, may move to foreground during execution. |
.foreground(.deferred) | Starts in background, opens app after perform() completes. |
Dynamic Foreground Transition
Start in background, move to foreground only if needed:
struct EditDocumentIntent: AppIntent {
static var title: LocalizedStringResource = "Edit Document"
@Parameter(title: "Document")
var document: DocumentEntity
// Start background, but can transition to foreground
var supportedModes: IntentModes { .foreground(.dynamic) }
func perform() async throws -> some IntentResult {
let doc = try await DocumentStore.shared.fetch(id: document.id)
if doc.requiresAuthentication {
// Move to foreground to show auth UI
try await continueInForeground(alwaysConfirm: true)
await MainActor.run {
AppState.shared.showAuthThenEdit(document: doc)
}
} else {
// Stay in background
try await DocumentStore.shared.openForEditing(doc)
}
return .result()
}
}continueInForeground
Call this inside perform() to transition from background to foreground:
// Transition to foreground, ask user to confirm
try await continueInForeground(alwaysConfirm: true)
// Transition to foreground without confirmation
try await continueInForeground(alwaysConfirm: false)If the user declines (when alwaysConfirm: true), the method throws needsToContinueInForegroundError().
needsToContinueInForegroundError
When an intent absolutely must run in the foreground but was started in the background, throw this error to prompt the user:
func perform() async throws -> some IntentResult {
guard canRunInBackground else {
throw needsToContinueInForegroundError(
"This action requires the app to be open."
)
}
// background work
return .result()
}Patterns
// ✅ Good -- background intent for data work
var supportedModes: IntentModes { .background }
// ✅ Good -- immediate foreground for camera/AR features
var supportedModes: IntentModes { .foreground(.immediate) }
// ✅ Good -- dynamic for intents that might need UI
var supportedModes: IntentModes { .foreground(.dynamic) }
// ❌ Wrong -- using openAppWhenRun instead of supportedModes (legacy approach)
static var openAppWhenRun = trueNote: openAppWhenRun still works on older OS versions but supportedModes is the preferred API on iOS 26+.
Multiple Choice API
Present a set of options and let the user pick one. Available in iOS 26 / macOS 26.
requestChoice(between:dialog:)
struct PickPlaylistIntent: AppIntent {
static var title: LocalizedStringResource = "Pick Playlist"
func perform() async throws -> some IntentResult & ProvidesDialog {
let playlists = await MusicStore.shared.allPlaylists()
let options = playlists.map { playlist in
IntentChoiceOption(
value: playlist.id,
title: "\(playlist.name)",
subtitle: "\(playlist.trackCount) tracks"
)
}
let chosen = try await requestChoice(
between: options,
dialog: "Which playlist would you like to play?"
)
await MusicPlayer.shared.play(playlistID: chosen)
let playlist = playlists.first { $0.id == chosen }
return .result(dialog: "Now playing \(playlist?.name ?? "playlist").")
}
}IntentChoiceOption
Each option has a value, title, and optional subtitle/image:
// Text only
IntentChoiceOption(
value: item.id,
title: "\(item.name)"
)
// With subtitle
IntentChoiceOption(
value: item.id,
title: "\(item.name)",
subtitle: "\(item.detail)"
)
// With system image
IntentChoiceOption(
value: item.id,
title: "\(item.name)",
image: .init(systemName: "star.fill")
)Patterns
// ✅ Good -- descriptive dialog, meaningful option labels
let chosen = try await requestChoice(
between: options,
dialog: "Which account should I transfer from?"
)
// ❌ Wrong -- vague dialog, no context
let chosen = try await requestChoice(
between: options,
dialog: "Pick one"
)Property Macros
New property macros for smarter entity data handling. Available in iOS 26 / macOS 26.
@ComputedProperty
Computed from a source of truth. The system knows this value is derived and does not store it independently:
struct OrderEntity: AppEntity {
var id: String
var items: [OrderItem]
@ComputedProperty(title: "Total")
var total: Double {
items.reduce(0) { $0 + $1.price * Double($1.quantity) }
}
@ComputedProperty(title: "Item Count")
var itemCount: Int {
items.count
}
// ...display representations and defaultQuery
}@DeferredProperty
Expensive to compute, fetched on demand only when the system actually needs the value:
struct PhotoEntity: AppEntity {
var id: String
var name: String
@DeferredProperty(title: "File Size")
var fileSize: Int // Fetched lazily, only when requested
@DeferredProperty(title: "Dimensions")
var dimensions: String // e.g., "3024x4032"
// ...display representations and defaultQuery
}The system calls a separate fetch only when these properties are needed, avoiding upfront cost for listing entities.
Interactive Snippets
Show SwiftUI views in Siri results. Static snippets display information; interactive snippets accept user actions.
Static Snippets
Return a view from perform() using .result(view:):
struct WeatherIntent: AppIntent {
static var title: LocalizedStringResource = "Check Weather"
@Parameter(title: "City")
var city: String
func perform() async throws -> some IntentResult & ShowsSnippetView {
let weather = try await WeatherService.shared.fetch(city: city)
return .result(view: WeatherSnippetView(weather: weather))
}
}
struct WeatherSnippetView: View {
let weather: WeatherData
var body: some View {
VStack(alignment: .leading, spacing: 8) {
HStack {
Image(systemName: weather.symbolName)
.font(.title)
Text(weather.city)
.font(.headline)
}
Text("\(weather.temperature, format: .number)°")
.font(.system(size: 48, weight: .thin))
Text(weather.condition)
.foregroundStyle(.secondary)
}
.padding()
}
}Interactive Snippets with SnippetIntent
Use SnippetIntent to add buttons and controls that trigger follow-up actions:
// The main intent shows the snippet
struct ShowTimerIntent: AppIntent {
static var title: LocalizedStringResource = "Show Timer"
func perform() async throws -> some IntentResult & ShowsSnippetView {
let timer = await TimerService.shared.activeTimer()
return .result(view: TimerSnippetView(timer: timer))
}
}
// A snippet intent handles button taps within the snippet
struct PauseTimerSnippetIntent: AppIntent, SnippetIntent {
static var title: LocalizedStringResource = "Pause Timer"
func perform() async throws -> some IntentResult & ShowsSnippetView {
let timer = await TimerService.shared.pauseActive()
// Return updated snippet view
return .result(view: TimerSnippetView(timer: timer))
}
}
struct ResumeTimerSnippetIntent: AppIntent, SnippetIntent {
static var title: LocalizedStringResource = "Resume Timer"
func perform() async throws -> some IntentResult & ShowsSnippetView {
let timer = await TimerService.shared.resumeActive()
return .result(view: TimerSnippetView(timer: timer))
}
}
// The snippet view uses IntentButton to trigger snippet intents
struct TimerSnippetView: View {
let timer: TimerState
var body: some View {
VStack(spacing: 12) {
Text(timer.remaining, format: .time(pattern: .minuteSecond))
.font(.system(size: 36, weight: .medium, design: .monospaced))
HStack(spacing: 16) {
if timer.isRunning {
IntentButton(intent: PauseTimerSnippetIntent()) {
Label("Pause", systemImage: "pause.fill")
}
.buttonStyle(.bordered)
} else {
IntentButton(intent: ResumeTimerSnippetIntent()) {
Label("Resume", systemImage: "play.fill")
}
.buttonStyle(.borderedProminent)
}
}
}
.padding()
}
}Key rules for interactive snippets:
- Button actions must use
SnippetIntentconformance, not plainAppIntent - Use
IntentButton(notButton) to trigger intents from within snippet views - The snippet intent's
perform()returns an updated snippet view - Keep snippet views lightweight; they run in a constrained environment
Patterns
// ✅ Good -- SnippetIntent for interactive buttons
struct LikeSnippetIntent: AppIntent, SnippetIntent {
static var title: LocalizedStringResource = "Like"
// ...
}
// ❌ Wrong -- plain AppIntent used in a snippet button
struct LikeIntent: AppIntent { // Missing SnippetIntent conformance
static var title: LocalizedStringResource = "Like"
// IntentButton with this intent will not work in a snippet
}
// ✅ Good -- IntentButton in snippet view
IntentButton(intent: LikeSnippetIntent()) {
Label("Like", systemImage: "heart")
}
// ❌ Wrong -- regular Button in snippet view (cannot trigger intents)
Button("Like") {
// This closure runs in the snippet process, not your app
}Visual Intelligence Integration
Let your app's content appear when users point their camera at objects. Uses IntentValueQuery with SemanticContentDescriptor. Requires iOS 18.
For full Visual Intelligence details, see apple-intelligence/visual-intelligence/SKILL.md.
@UnionValue for Multiple Result Types
When your app can return different entity types from a visual search:
import AppIntents
import VisualIntelligence
@UnionValue
enum ShopResult {
case product(ProductEntity)
case brand(BrandEntity)
case store(StoreEntity)
}
struct ShopVisualSearchQuery: IntentValueQuery {
func values(for input: SemanticContentDescriptor) async throws -> [ShopResult] {
var results: [ShopResult] = []
let products = await ProductStore.shared.search(labels: input.labels)
results.append(contentsOf: products.prefix(10).map { .product($0) })
let brands = await BrandStore.shared.search(labels: input.labels)
results.append(contentsOf: brands.prefix(5).map { .brand($0) })
return results
}
}OpenIntent per Entity Type
For each entity type in a union, provide an OpenIntent so users can tap to open:
struct OpenProductIntent: AppIntent, OpenIntent {
static var title: LocalizedStringResource = "Open Product"
static var openAppWhenRun = true
@Parameter(title: "Product")
var target: ProductEntity
func perform() async throws -> some IntentResult {
await MainActor.run {
NavigationState.shared.navigate(to: .product(id: target.id))
}
return .result()
}
}
struct OpenBrandIntent: AppIntent, OpenIntent {
static var title: LocalizedStringResource = "Open Brand"
static var openAppWhenRun = true
@Parameter(title: "Brand")
var target: BrandEntity
func perform() async throws -> some IntentResult {
await MainActor.run {
NavigationState.shared.navigate(to: .brand(id: target.id))
}
return .result()
}
}Onscreen Entities
Associate visible app content with entity identifiers so Siri and ChatGPT can reference what is currently on screen. Available in iOS 26 / macOS 26.
.userActivity() Modifier
Attach an EntityIdentifier to views so the system knows which entity is displayed:
struct RecipeDetailView: View {
let recipe: Recipe
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
Text(recipe.name).font(.largeTitle)
Text(recipe.description)
IngredientsListView(ingredients: recipe.ingredients)
StepsListView(steps: recipe.steps)
}
.padding()
}
.userActivity("com.myapp.viewRecipe") { activity in
activity.title = recipe.name
activity.targetContentIdentifier = recipe.id
// Associate with the AppEntity
activity.appEntity = EntityIdentifier(for: RecipeEntity.self, identifier: recipe.id)
}
}
}EntityIdentifier
Creates a typed reference connecting an on-screen element to an AppEntity:
// Create identifier for a specific entity instance
let identifier = EntityIdentifier(for: RecipeEntity.self, identifier: recipe.id)
// Use in NSUserActivity
activity.appEntity = identifierThis enables Siri to say "Tell me about this recipe" while looking at the screen, and the system routes the query to your entity.
Patterns
// ✅ Good -- entity identifier matches actual displayed content
.userActivity("com.myapp.viewItem") { activity in
activity.appEntity = EntityIdentifier(
for: ItemEntity.self,
identifier: item.id
)
}
// ❌ Wrong -- generic activity with no entity association
.userActivity("com.myapp.viewItem") { activity in
activity.title = item.name
// No entity identifier -- Siri cannot reference this content
}Swift Package Support
Share intents across apps or with extensions using AppIntentsPackage. Available in iOS 26 / macOS 26.
AppIntentsPackage Protocol
// In your Swift package
public struct SharedIntentsPackage: AppIntentsPackage {
// List other packages this one depends on
public static var includedPackages: [any AppIntentsPackage.Type] {
[]
}
}Including Packages in Your App
// In your app target
struct MyAppIntentsPackage: AppIntentsPackage {
static var includedPackages: [any AppIntentsPackage.Type] {
[SharedIntentsPackage.self]
}
}Use Case
Package support is useful when:
- Multiple apps share the same entity types or intents
- App extensions need access to the same intents as the main app
- You distribute reusable intent functionality as a Swift package
Patterns
// ✅ Good -- package declares its dependencies
struct AnalyticsIntentsPackage: AppIntentsPackage {
static var includedPackages: [any AppIntentsPackage.Type] {
[CoreDataIntentsPackage.self]
}
}
// ❌ Wrong -- intents duplicated across targets instead of shared via package
// App target: struct FavoriteIntent: AppIntent { ... }
// Widget target: struct FavoriteIntent: AppIntent { ... } // Duplicate!Complete Example: Music Player with Interactive Snippet
import AppIntents
import SwiftUI
// MARK: - Entity
struct SongEntity: AppEntity {
var id: String
var title: String
var artist: String
var albumArt: String
static var typeDisplayRepresentation: TypeDisplayRepresentation {
TypeDisplayRepresentation(
name: LocalizedStringResource("Song"),
numericFormat: "\(placeholder: .int) songs"
)
}
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(
title: "\(title)",
subtitle: "\(artist)",
image: .init(named: albumArt)
)
}
static var defaultQuery = SongEntityQuery()
}
struct SongEntityQuery: EntityStringQuery {
func entities(for identifiers: [String]) async throws -> [SongEntity] {
await MusicStore.shared.songs(for: identifiers)
}
func entities(matching string: String) async throws -> [SongEntity] {
await MusicStore.shared.search(query: string)
}
func suggestedEntities() async throws -> [SongEntity] {
await MusicStore.shared.recentlyPlayed(limit: 10)
}
}
// MARK: - Play Intent with Snippet
struct PlaySongIntent: AppIntent {
static var title: LocalizedStringResource = "Play Song"
@Parameter(title: "Song")
var song: SongEntity
var supportedModes: IntentModes { .background }
func perform() async throws -> some IntentResult & ShowsSnippetView {
await MusicPlayer.shared.play(songID: song.id)
let state = await MusicPlayer.shared.currentState()
return .result(view: NowPlayingSnippetView(state: state))
}
}
// MARK: - Snippet Intents
struct PauseSongSnippetIntent: AppIntent, SnippetIntent {
static var title: LocalizedStringResource = "Pause"
var supportedModes: IntentModes { .background }
func perform() async throws -> some IntentResult & ShowsSnippetView {
await MusicPlayer.shared.pause()
let state = await MusicPlayer.shared.currentState()
return .result(view: NowPlayingSnippetView(state: state))
}
}
struct SkipSongSnippetIntent: AppIntent, SnippetIntent {
static var title: LocalizedStringResource = "Skip"
var supportedModes: IntentModes { .background }
func perform() async throws -> some IntentResult & ShowsSnippetView {
await MusicPlayer.shared.skipToNext()
let state = await MusicPlayer.shared.currentState()
return .result(view: NowPlayingSnippetView(state: state))
}
}
// MARK: - Snippet View
struct NowPlayingSnippetView: View {
let state: PlayerState
var body: some View {
HStack(spacing: 12) {
Image(state.albumArt)
.resizable()
.frame(width: 60, height: 60)
.clipShape(RoundedRectangle(cornerRadius: 8))
VStack(alignment: .leading, spacing: 4) {
Text(state.title)
.font(.headline)
.lineLimit(1)
Text(state.artist)
.font(.subheadline)
.foregroundStyle(.secondary)
.lineLimit(1)
}
Spacer()
HStack(spacing: 12) {
if state.isPlaying {
IntentButton(intent: PauseSongSnippetIntent()) {
Image(systemName: "pause.fill")
.font(.title2)
}
}
IntentButton(intent: SkipSongSnippetIntent()) {
Image(systemName: "forward.fill")
.font(.title2)
}
}
}
.padding()
}
}
// MARK: - App Shortcuts
struct MusicShortcuts: AppShortcutsProvider {
static var appShortcuts: [AppShortcut] {
AppShortcut(
intent: PlaySongIntent(),
phrases: [
"Play \(\.$song) in \(.applicationName)",
"Listen to \(\.$song) on \(.applicationName)"
],
shortTitle: "Play Song",
systemImageName: "play.fill"
)
}
}References
- SnippetIntent protocol
- IntentModes
- AppIntentsPackage
- Visual Intelligence integration
- NSUserActivity
/Users/ravishankar/Downloads/docs/AppIntents-Updates.md
Entities and Spotlight Indexing
Define searchable data entities with AppEntity and make them discoverable in Spotlight with IndexedEntity, @Property, and CSSearchableIndex.
AppEntity Protocol
Every entity must provide an ID, display representations, and a default query.
Basic Entity
import AppIntents
struct RecipeEntity: AppEntity {
var id: String
var name: String
var cuisine: String
var prepTime: Int
static var typeDisplayRepresentation: TypeDisplayRepresentation {
TypeDisplayRepresentation(
name: LocalizedStringResource("Recipe"),
numericFormat: "\(placeholder: .int) recipes"
)
}
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(
title: "\(name)",
subtitle: "\(cuisine) - \(prepTime) min"
)
}
static var defaultQuery = RecipeEntityQuery()
}Display Representations
Control how entities appear across the system:
// Text only
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(
title: "\(name)",
subtitle: "\(category)"
)
}
// With image from asset catalog
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(
title: "\(name)",
subtitle: "\(formattedPrice)",
image: .init(named: imageName)
)
}
// With SF Symbol
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(
title: "\(name)",
subtitle: "\(status)",
image: .init(systemName: "doc.text")
)
}TypeDisplayRepresentation
Tells the system how to label this entity type collectively:
static var typeDisplayRepresentation: TypeDisplayRepresentation {
TypeDisplayRepresentation(
name: LocalizedStringResource("Recipe"),
numericFormat: "\(placeholder: .int) recipes"
)
}The numericFormat is used when Siri says things like "I found 5 recipes."
Entity Queries
Entities need queries so Siri and Shortcuts can find them. Choose the right query protocol based on how users will discover entities.
EntityStringQuery (Text Search)
Users type or speak a name to find the entity:
struct RecipeEntityQuery: EntityStringQuery {
func entities(for identifiers: [String]) async throws -> [RecipeEntity] {
let recipes = await RecipeStore.shared.recipes(for: identifiers)
return recipes.map { RecipeEntity(from: $0) }
}
func entities(matching string: String) async throws -> [RecipeEntity] {
let recipes = await RecipeStore.shared.search(query: string)
return recipes.map { RecipeEntity(from: $0) }
}
func suggestedEntities() async throws -> [RecipeEntity] {
let recent = await RecipeStore.shared.recentRecipes(limit: 10)
return recent.map { RecipeEntity(from: $0) }
}
}EntityPropertyQuery (Filter by Properties)
Users filter by specific attributes. Useful when entities have structured, filterable data:
struct RecipePropertyQuery: EntityPropertyQuery {
static var properties = QueryProperties {
Property(\RecipeEntity.$cuisine) {
EqualToComparator { $0 }
}
Property(\RecipeEntity.$prepTime) {
LessThanOrEqualToComparator { $0 }
}
}
static var sortingOptions = SortingOptions {
SortableBy(\RecipeEntity.$name)
SortableBy(\RecipeEntity.$prepTime)
}
func entities(
matching comparators: [NSPredicate],
mode: ComparatorMode,
sortedBy: [Sort<RecipeEntity>],
limit: Int?
) async throws -> [RecipeEntity] {
// Apply comparators to fetch matching entities
await RecipeStore.shared.query(
predicates: comparators,
sorts: sortedBy,
limit: limit
)
}
func entities(for identifiers: [String]) async throws -> [RecipeEntity] {
await RecipeStore.shared.recipes(for: identifiers)
.map { RecipeEntity(from: $0) }
}
func suggestedEntities() async throws -> [RecipeEntity] {
await RecipeStore.shared.recentRecipes(limit: 10)
.map { RecipeEntity(from: $0) }
}
}Query Pattern Selection
| Query Type | Use When | Example |
|---|---|---|
EntityStringQuery | User searches by name/text | "Open recipe Pasta Carbonara" |
EntityPropertyQuery | User filters by attributes | "Show Italian recipes under 30 minutes" |
IndexedEntity for Spotlight
Make entities appear in Spotlight search results. Requires iOS 18 / macOS 15.
Conforming to IndexedEntity
import AppIntents
import CoreSpotlight
struct RecipeEntity: IndexedEntity {
var id: String
@Property(title: "Name")
var name: String
@Property(title: "Cuisine")
var cuisine: String
@Property(title: "Prep Time")
var prepTime: Int
@Property(title: "Ingredients")
var ingredients: [String]
static var typeDisplayRepresentation: TypeDisplayRepresentation {
TypeDisplayRepresentation(
name: LocalizedStringResource("Recipe"),
numericFormat: "\(placeholder: .int) recipes"
)
}
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(
title: "\(name)",
subtitle: "\(cuisine) - \(prepTime) min"
)
}
static var defaultQuery = RecipeEntityQuery()
}The @Property Macro
@Property declares entity fields and provides metadata for the system:
// Basic property
@Property(title: "Name")
var name: String
// Property with indexing key for Spotlight
@Property(title: "Author", indexingKey: \.authorNames)
var author: String
// Property with content type hint
@Property(title: "URL", indexingKey: \.url)
var websiteURL: URL?Indexing Keys
The indexingKey maps your property to a CSSearchableItemAttributeSet key path, telling Spotlight how to index the value:
struct ArticleEntity: IndexedEntity {
var id: String
@Property(title: "Title", indexingKey: \.title)
var title: String
@Property(title: "Summary", indexingKey: \.contentDescription)
var summary: String
@Property(title: "Author", indexingKey: \.authorNames)
var author: String
@Property(title: "Date", indexingKey: \.contentCreationDate)
var publishDate: Date
@Property(title: "URL", indexingKey: \.url)
var articleURL: URL?
// ...display representations and defaultQuery
}Common CSSearchableItemAttributeSet key paths:
| Key Path | Type | Purpose |
|---|---|---|
\.title | String? | Primary display title |
\.contentDescription | String? | Summary text |
\.authorNames | [String]? | Author names |
\.contentCreationDate | Date? | Creation date |
\.contentModificationDate | Date? | Last modified date |
\.url | URL? | Associated URL |
\.thumbnailData | Data? | Thumbnail image data |
\.keywords | [String]? | Searchable keywords |
\.contentType | String? | UTI content type |
Attribute Sets for Rich Metadata
For additional metadata beyond @Property indexing keys, provide a CSSearchableItemAttributeSet:
extension RecipeEntity {
var attributeSet: CSSearchableItemAttributeSet {
let attributes = CSSearchableItemAttributeSet()
attributes.title = name
attributes.contentDescription = "A \(cuisine) recipe ready in \(prepTime) minutes"
attributes.keywords = ingredients
if let imageData = loadThumbnail() {
attributes.thumbnailData = imageData
}
return attributes
}
}Triggering Indexing
After creating, updating, or deleting entities, tell Spotlight to reindex.
Index All Entities of a Type
// Reindex all recipes
try await CSSearchableIndex.default().indexAppEntities(of: RecipeEntity.self)Delete Entities from Index
// Remove all entities of a type
try await CSSearchableIndex.default().deleteAppEntities(of: RecipeEntity.self)
// Remove specific entities by ID
try await CSSearchableIndex.default().deleteAppEntities(
of: RecipeEntity.self,
identifiers: ["recipe-123", "recipe-456"]
)When to Reindex
Call indexAppEntities() at these points:
// After saving new or updated content
func saveRecipe(_ recipe: Recipe) async throws {
try await database.save(recipe)
try await CSSearchableIndex.default().indexAppEntities(of: RecipeEntity.self)
}
// After deleting content
func deleteRecipe(_ id: String) async throws {
try await database.delete(id)
try await CSSearchableIndex.default().indexAppEntities(of: RecipeEntity.self)
}
// On app launch if data may have changed externally
func applicationDidFinishLaunching() {
Task {
try? await CSSearchableIndex.default().indexAppEntities(of: RecipeEntity.self)
}
}Patterns
✅ Good Patterns
// Entity with complete metadata for Spotlight
struct NoteEntity: IndexedEntity {
var id: String
@Property(title: "Title", indexingKey: \.title)
var title: String
@Property(title: "Content", indexingKey: \.contentDescription)
var body: String
@Property(title: "Modified", indexingKey: \.contentModificationDate)
var modifiedDate: Date
@Property(title: "Tags", indexingKey: \.keywords)
var tags: [String]
static var defaultQuery = NoteEntityQuery()
// ...display representations
}
// Reindex after every mutation
func save(_ note: Note) async throws {
try await database.save(note)
try await CSSearchableIndex.default().indexAppEntities(of: NoteEntity.self)
}❌ Anti-Patterns
// Missing defaultQuery -- entity cannot be resolved
struct BrokenEntity: AppEntity {
var id: String
var name: String
// No defaultQuery, no display representations
}
// Properties without indexing keys -- Spotlight cannot search these
struct WeakEntity: IndexedEntity {
var id: String
@Property(title: "Title")
var title: String // Not indexed -- missing indexingKey
@Property(title: "Body")
var body: String // Not indexed -- missing indexingKey
}
// Never reindexing after data changes
func save(_ note: Note) async throws {
try await database.save(note)
// Spotlight still shows stale data
}Complete Example: Bookmark Manager
import AppIntents
import CoreSpotlight
// MARK: - Entity
struct BookmarkEntity: IndexedEntity {
var id: String
@Property(title: "Title", indexingKey: \.title)
var title: String
@Property(title: "URL", indexingKey: \.url)
var url: URL
@Property(title: "Description", indexingKey: \.contentDescription)
var summary: String
@Property(title: "Tags", indexingKey: \.keywords)
var tags: [String]
@Property(title: "Added", indexingKey: \.contentCreationDate)
var dateAdded: Date
static var typeDisplayRepresentation: TypeDisplayRepresentation {
TypeDisplayRepresentation(
name: LocalizedStringResource("Bookmark"),
numericFormat: "\(placeholder: .int) bookmarks"
)
}
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(
title: "\(title)",
subtitle: "\(url.host ?? url.absoluteString)",
image: .init(systemName: "bookmark.fill")
)
}
static var defaultQuery = BookmarkEntityQuery()
}
// MARK: - Query
struct BookmarkEntityQuery: EntityStringQuery {
func entities(for identifiers: [String]) async throws -> [BookmarkEntity] {
let bookmarks = await BookmarkStore.shared.bookmarks(for: identifiers)
return bookmarks.map { BookmarkEntity(from: $0) }
}
func entities(matching string: String) async throws -> [BookmarkEntity] {
let bookmarks = await BookmarkStore.shared.search(query: string)
return bookmarks.map { BookmarkEntity(from: $0) }
}
func suggestedEntities() async throws -> [BookmarkEntity] {
let recent = await BookmarkStore.shared.recentBookmarks(limit: 15)
return recent.map { BookmarkEntity(from: $0) }
}
}
// MARK: - Indexing
enum BookmarkIndexer {
static func reindex() async throws {
try await CSSearchableIndex.default().indexAppEntities(
of: BookmarkEntity.self
)
}
static func clearIndex() async throws {
try await CSSearchableIndex.default().deleteAppEntities(
of: BookmarkEntity.self
)
}
}
// MARK: - Intent
struct SaveBookmarkIntent: AppIntent {
static var title: LocalizedStringResource = "Save Bookmark"
static var description: IntentDescription = "Saves a URL as a bookmark"
@Parameter(title: "URL")
var url: URL
@Parameter(title: "Title")
var bookmarkTitle: String
@Parameter(title: "Tags")
var tags: [String]?
func perform() async throws -> some IntentResult & ReturnsValue<BookmarkEntity> & ProvidesDialog {
let bookmark = try await BookmarkStore.shared.save(
url: url,
title: bookmarkTitle,
tags: tags ?? []
)
// Reindex so Spotlight picks up the new bookmark
try await BookmarkIndexer.reindex()
let entity = BookmarkEntity(from: bookmark)
return .result(
value: entity,
dialog: "Saved bookmark: \(bookmarkTitle)"
)
}
}References
App Intents Basics
Core patterns for the AppIntent protocol, parameters, the perform() method, and App Shortcuts with voice phrases.
The AppIntent Protocol
Every intent conforms to AppIntent and must provide a static title and a perform() method.
Minimal Intent
import AppIntents
struct OpenSettingsIntent: AppIntent {
static var title: LocalizedStringResource = "Open Settings"
static var description: IntentDescription = "Opens the app settings screen"
func perform() async throws -> some IntentResult {
await MainActor.run {
NavigationState.shared.navigate(to: .settings)
}
return .result()
}
}Intent with Dialog Result
Return a spoken/displayed response to the user:
struct CheckBalanceIntent: AppIntent {
static var title: LocalizedStringResource = "Check Balance"
func perform() async throws -> some IntentResult & ReturnsValue<String> & ProvidesDialog {
let balance = await AccountService.shared.currentBalance()
return .result(
value: balance.formatted,
dialog: "Your balance is \(balance.formatted)."
)
}
}Intent that Opens the App
struct ComposeMessageIntent: AppIntent {
static var title: LocalizedStringResource = "Compose Message"
static var openAppWhenRun = true
func perform() async throws -> some IntentResult {
await MainActor.run {
AppState.shared.startNewMessage()
}
return .result()
}
}Parameters
Use @Parameter to accept input from Siri or Shortcuts.
Basic Parameter Types
struct CreateReminderIntent: AppIntent {
static var title: LocalizedStringResource = "Create Reminder"
@Parameter(title: "Title")
var reminderTitle: String
@Parameter(title: "Due Date")
var dueDate: Date?
@Parameter(title: "Priority", default: .medium)
var priority: ReminderPriority
@Parameter(title: "Notes")
var notes: String?
func perform() async throws -> some IntentResult & ProvidesDialog {
let reminder = Reminder(
title: reminderTitle,
dueDate: dueDate,
priority: priority,
notes: notes
)
try await ReminderStore.shared.save(reminder)
return .result(dialog: "Created reminder: \(reminderTitle)")
}
}Entity Parameters
Reference an AppEntity as a parameter so Siri can resolve it:
struct OpenNoteIntent: AppIntent {
static var title: LocalizedStringResource = "Open Note"
static var openAppWhenRun = true
@Parameter(title: "Note")
var note: NoteEntity
func perform() async throws -> some IntentResult {
await MainActor.run {
NavigationState.shared.navigate(to: .note(id: note.id))
}
return .result()
}
}Enum Parameters
Enums used as parameters must conform to AppEnum:
enum ReminderPriority: String, AppEnum {
case low
case medium
case high
static var typeDisplayRepresentation: TypeDisplayRepresentation {
TypeDisplayRepresentation(name: "Priority")
}
static var caseDisplayRepresentations: [ReminderPriority: DisplayRepresentation] {
[
.low: "Low",
.medium: "Medium",
.high: "High"
]
}
}Parameter Validation
Validate input inside perform() and throw an error with a user-facing dialog:
func perform() async throws -> some IntentResult & ProvidesDialog {
guard !reminderTitle.trimmingCharacters(in: .whitespaces).isEmpty else {
throw $reminderTitle.needsValueError("Please provide a title for the reminder.")
}
guard reminderTitle.count <= 200 else {
throw IntentError.custom(
localizedDescription: "Title must be 200 characters or fewer."
)
}
// proceed with valid input
try await ReminderStore.shared.save(reminder)
return .result(dialog: "Created: \(reminderTitle)")
}The perform() Method
perform() is the entry point when the intent runs. It must be async throws and return some IntentResult.
Return Types
| Return Type | Use Case |
|---|---|
.result() | No output needed |
.result(dialog:) | Spoken/displayed text |
.result(value:) | Return a value for Shortcuts chaining |
.result(value:dialog:) | Return value and speak dialog |
.result(view:) | Show a SwiftUI snippet view |
.result(value:dialog:view:) | All of the above |
Returning a Value
When your intent produces output that another Shortcut can consume:
struct CountItemsIntent: AppIntent {
static var title: LocalizedStringResource = "Count Items"
@Parameter(title: "List")
var list: ListEntity
func perform() async throws -> some IntentResult & ReturnsValue<Int> {
let count = await ListStore.shared.itemCount(for: list.id)
return .result(value: count)
}
}Error Handling in perform()
func perform() async throws -> some IntentResult & ProvidesDialog {
do {
let result = try await service.doWork()
return .result(dialog: "Done: \(result.summary)")
} catch ServiceError.notAuthenticated {
throw IntentError.custom(
localizedDescription: "Please sign in to your account first."
)
} catch ServiceError.networkUnavailable {
throw IntentError.custom(
localizedDescription: "No network connection. Please try again later."
)
} catch {
throw IntentError.custom(
localizedDescription: "Something went wrong. Please try again."
)
}
}App Shortcuts
App Shortcuts let users invoke intents with specific voice phrases without any setup.
AppShortcutsProvider
struct MyAppShortcuts: AppShortcutsProvider {
static var appShortcuts: [AppShortcut] {
AppShortcut(
intent: CheckBalanceIntent(),
phrases: [
"Check my balance in \(.applicationName)",
"What's my \(.applicationName) balance"
],
shortTitle: "Check Balance",
systemImageName: "creditcard"
)
AppShortcut(
intent: CreateReminderIntent(),
phrases: [
"Create a reminder in \(.applicationName)",
"Add a \(.applicationName) reminder"
],
shortTitle: "New Reminder",
systemImageName: "plus.circle"
)
}
}Phrase Guidelines
Follow these rules for natural-sounding phrases:
// ✅ Good phrases -- natural, include app name placeholder
"Check my balance in \(.applicationName)"
"Start a workout with \(.applicationName)"
"Open \(.applicationName) settings"
// ❌ Bad phrases -- unnatural, missing app name, too generic
"Do the thing" // No app name, too vague
"Check balance" // Missing \(.applicationName)
"Please check my balance now" // Overly conversationalRules:
- Always include
\(.applicationName)so the system binds the phrase to your app - Keep phrases short and direct (3-8 words)
- Use natural sentence fragments users would actually say
- Provide 2-3 phrase variations per shortcut
- Avoid filler words like "please" or "now"
Parameterized Phrases
Include parameters in voice phrases using entity references:
AppShortcut(
intent: OpenNoteIntent(),
phrases: [
"Open \(\.$note) in \(.applicationName)",
"Show my \(\.$note) note in \(.applicationName)"
],
shortTitle: "Open Note",
systemImageName: "doc.text"
)The system resolves \(\.$note) by querying the entity's defaultQuery with what the user said.
OpenIntent for Entities
When you have an entity type that users open or view, conform to OpenIntent:
struct OpenRecipeIntent: AppIntent, OpenIntent {
static var title: LocalizedStringResource = "Open Recipe"
@Parameter(title: "Recipe")
var target: RecipeEntity
static var openAppWhenRun = true
func perform() async throws -> some IntentResult {
await MainActor.run {
NavigationState.shared.navigate(to: .recipe(id: target.id))
}
return .result()
}
}This enables "Open [recipe name] in [App Name]" automatically for all recipes.
Patterns
✅ Good Patterns
// Clear, descriptive title
static var title: LocalizedStringResource = "Add Item to Shopping List"
// Descriptive parameter titles
@Parameter(title: "Item Name")
var itemName: String
// Meaningful dialog responses
return .result(dialog: "Added \(itemName) to your shopping list.")
// Optional parameters with sensible defaults
@Parameter(title: "Quantity", default: 1)
var quantity: Int❌ Anti-Patterns
// Vague title
static var title: LocalizedStringResource = "Do Action"
// Missing parameter title
@Parameter
var x: String
// Silent result when user expects feedback
return .result() // User said "Add milk" and got no confirmation
// Blocking the main thread
func perform() async throws -> some IntentResult {
let result = heavyComputation() // Not async, blocks
return .result(value: result)
}Complete Example: Task Manager
import AppIntents
// MARK: - Entity
struct TaskEntity: AppEntity {
var id: String
var title: String
var isComplete: Bool
var dueDate: Date?
static var typeDisplayRepresentation: TypeDisplayRepresentation {
TypeDisplayRepresentation(
name: LocalizedStringResource("Task"),
numericFormat: "\(placeholder: .int) tasks"
)
}
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(
title: "\(title)",
subtitle: isComplete ? "Completed" : "Pending"
)
}
static var defaultQuery = TaskEntityQuery()
}
// MARK: - Entity Query
struct TaskEntityQuery: EntityStringQuery {
func entities(for identifiers: [String]) async throws -> [TaskEntity] {
await TaskStore.shared.tasks(for: identifiers)
}
func entities(matching string: String) async throws -> [TaskEntity] {
await TaskStore.shared.search(matching: string)
}
func suggestedEntities() async throws -> [TaskEntity] {
await TaskStore.shared.recentTasks(limit: 10)
}
}
// MARK: - Intents
struct AddTaskIntent: AppIntent {
static var title: LocalizedStringResource = "Add Task"
static var description: IntentDescription = "Creates a new task"
@Parameter(title: "Title")
var taskTitle: String
@Parameter(title: "Due Date")
var dueDate: Date?
func perform() async throws -> some IntentResult & ReturnsValue<TaskEntity> & ProvidesDialog {
let task = try await TaskStore.shared.create(
title: taskTitle,
dueDate: dueDate
)
let entity = TaskEntity(
id: task.id,
title: task.title,
isComplete: false,
dueDate: task.dueDate
)
return .result(
value: entity,
dialog: "Created task: \(taskTitle)"
)
}
}
struct CompleteTaskIntent: AppIntent {
static var title: LocalizedStringResource = "Complete Task"
static var description: IntentDescription = "Marks a task as complete"
@Parameter(title: "Task")
var task: TaskEntity
func perform() async throws -> some IntentResult & ProvidesDialog {
try await TaskStore.shared.markComplete(id: task.id)
return .result(dialog: "Marked \(task.title) as complete.")
}
}
// MARK: - App Shortcuts
struct TaskShortcuts: AppShortcutsProvider {
static var appShortcuts: [AppShortcut] {
AppShortcut(
intent: AddTaskIntent(),
phrases: [
"Add a task in \(.applicationName)",
"Create a \(.applicationName) task"
],
shortTitle: "Add Task",
systemImageName: "plus.circle"
)
AppShortcut(
intent: CompleteTaskIntent(),
phrases: [
"Complete \(\.$task) in \(.applicationName)",
"Mark \(\.$task) done in \(.applicationName)"
],
shortTitle: "Complete Task",
systemImageName: "checkmark.circle"
)
}
}