Highlight
This session is a ground-up introduction to the App Intents framework. It starts from the three core concepts — intent, entity, and query — and works up to App Shortcut, Spotlight indexing, and SwiftUI navigation integration. The speaker uses a single landmark-browsing app as a running demo, and step by step builds out a complete intent system that can be invoked from Spotlight, Siri, and the Action Button.
Main Content
Many apps share the same pain point: to reach a specific page, the user has to open the app and dig through several layers of menus. Saying it to Siri, searching from Spotlight, or pressing the Action Button to jump straight there sounds great, but between the user and that page sits a whole layer of system capability that lives outside the app. App Intents is the framework Apple uses to bridge that gap.
The speaker, James, demos a landmark app. The app has three tabs: Landmarks, Map, and Collections. He starts with about twenty lines of code that wrap “jump to the Landmarks page” into a NavigateIntent. Once the app is installed, the action shows up in Shortcuts immediately. He then adds one line, static let supportedModes: IntentModes = .foreground, and the app gets pulled to the foreground at run time. Next he extracts the navigation target into an AppEnum called NavigationOption, uses @Parameter to let the user pick a destination, and adds an AppShortcutsProvider. With that, the same capability lights up in Siri, Spotlight, and the Action Button at once.
The framework’s design philosophy is to model an app’s features as verbs (intents) and nouns (entities), and then use queries to teach the system about your data. This year’s updates focus on four things: entity properties support @ComputedProperty, so you can read values lazily from an existing model instead of copying them; Spotlight supports an indexingKey annotation directly on @Property; the new TargetContentProvidingIntent protocol pairs with the onAppIntentExecution modifier for declarative navigation; and App Intents finally supports defining types in Swift packages and static libraries, with AppIntentsPackage registering shared types across targets.
Detailed Content
The smallest unit in App Intents is a struct conforming to the AppIntent protocol. It only requires two things: a title and a perform(). Here is the initial NavigateIntent from the demo (03:23):
struct NavigateIntent: AppIntent {
static let title: LocalizedStringResource = "Navigate to Landmarks"
static let supportedModes: IntentModes = .foreground
@MainActor
func perform() async throws -> some IntentResult {
Navigator.shared.navigate(to: .landmarks)
return .result()
}
}
Key points:
titlemust be a constant string. The framework reads source at compile time to generate metadata, so it cannot be a computed property.supportedModes = .foregroundpulls the app to the foreground before the intent runs. The default is to run in the background.@MainActormakes sureperformruns on the main thread, because navigation must happen on the main thread.- The returned
IntentResultcan carry a dialog (read aloud by Siri), a view snippet (rendered in the system pop-up), and aReturnsValue(used as input to the next intent).
A single fixed page is too rigid. The second step is to turn the destination into a parameter (05:38):
struct NavigateIntent: AppIntent {
static let title: LocalizedStringResource = "Navigate to Section"
static let supportedModes: IntentModes = .foreground
static var parameterSummary: some ParameterSummary {
Summary("Navigate to \(\.$navigationOption)")
}
@Parameter(
title: "Section",
requestValueDialog: "Which section?"
)
var navigationOption: NavigationOption
@MainActor
func perform() async throws -> some IntentResult {
Navigator.shared.navigate(to: navigationOption)
return .result()
}
}
Key points:
@Parameterdeclares a variable as input to the intent. WithoutOptional, it is required, and the system will ask the user for a value before callingperform.parameterSummaryusesSummary("Navigate to \(\.$navigationOption)")to thread the action and the parameter into one natural-language sentence. Shortcuts renders the parameter as a tappable inline control.requestValueDialogis the prompt Siri speaks when it asks the user for a value.- New rule this year: as long as an intent provides a complete parameter summary, it can run directly from macOS Spotlight (08:09).
The next step is to model the truly dynamic “nouns” in the app. The number of landmarks is not fixed, so they need an AppEntity. The new @ComputedProperty saves you from copying data from the model into the entity (11:02):
struct LandmarkEntity: AppEntity {
var id: Int { landmark.id }
@ComputedProperty
var name: String { landmark.name }
@ComputedProperty
var description: String { landmark.description }
let landmark: Landmark
static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "Landmark")
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(title: "\(name)")
}
static let defaultQuery = LandmarkEntityQuery()
}
Key points:
idmust be a stable, persistent identifier you can use to look up the record in your database. The system caches this id, so it can resolve back to the original entity after a restart.@ComputedPropertyis new this year. It acts like a getter, and Shortcuts only touches the model when it reads these fields, which avoids storing the same data twice.defaultQueryties the entity to anEntityQuery. The system uses this query to answer questions like “which entity does this id refer to.”
A query is the entity’s address. The most basic implementation answers “What is the entity for this ID” (13:19):
struct LandmarkEntityQuery: EntityQuery {
@Dependency var modelData: ModelData
func entities(for identifiers: [LandmarkEntity.ID]) async throws -> [LandmarkEntity] {
modelData
.landmarks(for: identifiers)
.map(LandmarkEntity.init)
}
}
Key points:
@Dependencyinjects an external data source into the query. You register it early in app startup withAppDependencyManager.shared.add { ModelData() }.entities(for:)is the one method every query must implement. The system hands it a list of ids and gets back entity instances.- On top of this you can adopt
EnumerableEntityQuery(return everything),EntityPropertyQuery(sort with predicates), andEntityStringQuery(match by string). These three protocols map to Find, Filter, and Search behaviors in Shortcuts.
The last piece is the declarative form of navigation. The new TargetContentProvidingIntent no longer requires a perform method. The navigation logic hangs off a SwiftUI view (18:17):
struct OpenLandmarkIntent: OpenIntent, TargetContentProvidingIntent {
static let title: LocalizedStringResource = "Open Landmark"
@Parameter(title: "Landmark", requestValueDialog: "Which landmark?")
var target: LandmarkEntity
}
struct LandmarksNavigationStack: View {
@State var path: [Landmark] = []
var body: some View {
NavigationStack(path: $path) {}
.onAppIntentExecution(OpenLandmarkIntent.self) { intent in
path.append(intent.target.landmark)
}
}
}
Key points:
- The
OpenIntentprotocol requires a parameter namedtarget, and pulls the app to the foreground beforeperformruns. TargetContentProvidingIntentlets you skip theperformimplementation and move “what to do once we have the parameter” into the view layer.- The
onAppIntentExecution(...)modifier listens for matching intents and updates the SwiftUI navigation path right inside the closure. That replaces the old glue code built around a globalNavigatorsingleton.
Key Takeaways
-
What to do: Add a minimal
AppShortcutto an existing app and expose “open my common pages” to Spotlight and Siri.- Why it is worth doing: based on the cost the session shows, this is a few dozen lines of Swift, but the user experience improves right away — opening the app no longer requires tapping the home tab and then a sub-tab, and the Action Button can bind to it directly.
- How to start: write a no-parameter version of the
NavigateIntentfrom 03:23, then attach a phrase inAppShortcutsProvider. Once installed, the action shows up in Shortcuts immediately.
-
What to do: Model the recurring “domain objects” in your app (orders, notes, favorites, places) as
AppEntityand adoptIndexedEntity.- Why it is worth doing: once you adopt
IndexedEntity, the system writes the entities into the Spotlight index for you and supports semantic search. Users can find your content from the lock-screen pull-down, which lifts your app’s reach to the system level. - How to start: wrap existing model fields with
@ComputedPropertyfirst to avoid double-writing data, add@Property(indexingKey:)to the key attributes, and finally call thedonatemethod onCSSearchableIndex.
- Why it is worth doing: once you adopt
-
What to do: Move in-app navigation logic from a global singleton to
TargetContentProvidingIntent+onAppIntentExecution.- Why it is worth doing: this declarative form lets deep link, Spotlight tap, and Siri voice share the same navigation code, which removes the need to hand-roll a router. SwiftUI’s observable state is preserved at the same time.
- How to start: pick the most common “open detail page” flow, define an
OpenXxxIntentthat adopts both protocols, attachonAppIntentExecutionto aNavigationStack, and append to the path inside the closure.
-
What to do: Adopt
Transferableon your entity to expose images, documents, and other representative content to Shortcuts and other apps.- Why it is worth doing: users can feed your entity directly into Photos, Messages, Mail, and other system actions, and multi-step Shortcuts can chain things automatically. This is cross-app interoperability you get for free.
- How to start: follow the
DataRepresentation(exportedContentType: .image)example at 16:33 and declare the image data the entity already holds.
-
What to do: Move App Intents types into a Swift Package and use the
AppIntentsPackageprotocol to share them across targets.- Why it is worth doing: starting this year, packages and static libraries can both define intents. That makes it easier to decouple the core domain model from UI. Multi-target projects (main app + extension + widget) can share one set of entities and avoid redeclaring them.
- How to start: move the entity files into an SPM target, define
struct XxxKitPackage: AppIntentsPackage {}, and pull it in throughincludedPackagesin the app target’s package.
Related Sessions
- Design interactive snippets — The companion design guide for snippet views in App Intents. It covers how to make the result of
performrender usefully and look good in the system. - Code-along: Bring on-device AI to your app using the Foundation Models framework — A code-along for wiring on-device large models into a SwiftUI app. App Intents is its common entry point.
- Deep dive into the Foundation Models framework — Advanced use of Foundation Models. Guided generation and tool calling share concepts with App Intents.
- Bring advanced speech-to-text to your app with SpeechAnalyzer — The new SpeechAnalyzer API, often combined with App Intents to wire speech through to actions.
Comments
GitHub Issues · utterances