WWDC Quick Look đź’“ By SwiftGGTeam
Get to know App Intents

Get to know App Intents

Watch original video

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:

  • title must be a constant string. The framework reads source at compile time to generate metadata, so it cannot be a computed property.
  • supportedModes = .foreground pulls the app to the foreground before the intent runs. The default is to run in the background.
  • @MainActor makes sure perform runs on the main thread, because navigation must happen on the main thread.
  • The returned IntentResult can carry a dialog (read aloud by Siri), a view snippet (rendered in the system pop-up), and a ReturnsValue (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:

  • @Parameter declares a variable as input to the intent. Without Optional, it is required, and the system will ask the user for a value before calling perform.
  • parameterSummary uses Summary("Navigate to \(\.$navigationOption)") to thread the action and the parameter into one natural-language sentence. Shortcuts renders the parameter as a tappable inline control.
  • requestValueDialog is 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:

  • id must 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.
  • @ComputedProperty is 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.
  • defaultQuery ties the entity to an EntityQuery. 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:

  • @Dependency injects an external data source into the query. You register it early in app startup with AppDependencyManager.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), and EntityStringQuery (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 OpenIntent protocol requires a parameter named target, and pulls the app to the foreground before perform runs.
  • TargetContentProvidingIntent lets you skip the perform implementation 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 global Navigator singleton.

Key Takeaways

  • What to do: Add a minimal AppShortcut to 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 NavigateIntent from 03:23, then attach a phrase in AppShortcutsProvider. 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 AppEntity and adopt IndexedEntity.

    • 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 @ComputedProperty first to avoid double-writing data, add @Property(indexingKey:) to the key attributes, and finally call the donate method on CSSearchableIndex.
  • 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 OpenXxxIntent that adopts both protocols, attach onAppIntentExecution to a NavigationStack, and append to the path inside the closure.
  • What to do: Adopt Transferable on 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 AppIntentsPackage protocol 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 through includedPackages in the app target’s package.

Comments

GitHub Issues · utterances