WWDC Quick Look 💓 By SwiftGGTeam
Dive into App Intents

Dive into App Intents

Watch original video

Highlight

SiriKit Intents introduced in iOS 10 were Objective-C based, required a separate Extension, and had high integration cost. App Intents completely redesigns this stack: define Intents in Swift directly in the main app target, with no Extension and no extra framework target.


Core Content

SiriKit Intents let apps connect to Siri, but they mainly work around fixed system domains such as messaging, fitness, and payments. When developers want to hand arbitrary app functionality to Siri, Spotlight, and Shortcuts, they often maintain extra definition files, Extensions, and shared frameworks.

App Intents turns this into Swift code. An Intent is a single action the app exposes to the system. An Entity represents an object in the app. An App Shortcut wraps an Intent into a discoverable, voice-triggerable entry point.

The talk uses a library app as an example. Users often open the “Currently Reading” shelf, so the first Intent does one thing: open that shelf. The code then expands step by step to open any shelf, open a book, add a book, find books by property, and return text and SwiftUI snippets to Siri.


Detailed Content

Write an Intent in Swift

(05:33) The minimal shape of an Intent is a Swift type conforming to AppIntent. It includes a title, a perform() method, and optional run behavior.

struct OpenCurrentlyReading: AppIntent {
    static var title: LocalizedStringResource = "Open Currently Reading"

    @MainActor
    func perform() async throws -> some IntentResult {
        Navigator.shared.openShelf(.currentlyReading)
        return .result()
    }
  
    static var openAppWhenRun: Bool = true
}

Key points:

  • struct OpenCurrentlyReading: AppIntent defines an action the system can run.
  • title is the localized title users see in the Shortcuts editor.
  • perform() is the code that actually runs when the Intent executes.
  • @MainActor runs navigation on the main thread because the example Navigator requires it.
  • Navigator.shared.openShelf(.currentlyReading) reuses existing in-app navigation logic.
  • return .result() tells the system the action completed.
  • openAppWhenRun = true launches the app when this UI-opening Intent runs.

(06:42) If you want users to discover this action in Siri and Spotlight without manually creating a Shortcut, also provide AppShortcutsProvider.

struct LibraryAppShortcuts: AppShortcutsProvider {
    static var appShortcuts: [AppShortcut] {
        AppShortcut(
            intent: OpenCurrentlyReading(),
            phrases: ["Open Currently Reading in \(.applicationName)"],
            systemImageName: "books.vertical.fill"
        )
    }
}

Key points:

  • LibraryAppShortcuts declares the set of shortcut actions the app provides.
  • AppShortcut turns an Intent into an entry point the system can show and invoke.
  • intent: OpenCurrentlyReading() binds the action defined above.
  • phrases defines phrases users can say to Siri.
  • \(.applicationName) inserts the app name into the spoken phrase.
  • systemImageName supplies an icon for Shortcuts and Spotlight.

Parameters let one Intent cover multiple targets

(07:11) If you can only open “Currently Reading,” the Intent is too narrow. Shelves are a fixed set, so an enum fits. To use an enum as an Intent parameter, conform to AppEnum.

enum Shelf: String {
    case currentlyReading
    case wantToRead
    case read
}

extension Shelf: AppEnum {
    static var typeDisplayRepresentation: TypeDisplayRepresentation = "Shelf"

    static var caseDisplayRepresentations: [Shelf: DisplayRepresentation] = [
        .currentlyReading: "Currently Reading",
        .wantToRead: "Want to Read",
        .read: "Read",
    ]
}

Key points:

  • Shelf: String gives each shelf a stable raw value.
  • AppEnum lets the enum be used as an Intent parameter.
  • typeDisplayRepresentation is the user-visible name of the enum type.
  • caseDisplayRepresentations supplies titles shown in Shortcuts for each case.
  • Dictionary literals are read by the compiler at build time to generate Intent metadata.

(07:49) The Intent receives this enum with @Parameter and uses it in perform().

struct OpenShelf: AppIntent {
    static var title: LocalizedStringResource = "Open Shelf"

    @Parameter(title: "Shelf")
    var shelf: Shelf

    @MainActor
    func perform() async throws -> some IntentResult {
        Navigator.shared.openShelf(shelf)
        return .result()
    }

    static var parameterSummary: some ParameterSummary {
        Summary("Open \(\.$shelf)")
    }

    static var openAppWhenRun: Bool = true
}

Key points:

  • @Parameter(title: "Shelf") declares user-configurable input.
  • var shelf: Shelf uses the enum exposed to App Intents above.
  • Navigator.shared.openShelf(shelf) lets one Intent open different shelves.
  • parameterSummary controls sentence-style display in the Shortcuts editor.
  • Summary("Open \(\.$shelf)") embeds the parameter in the summary for a more natural UI than table rows.
  • openAppWhenRun = true suits actions that need to show app UI.

Entity represents dynamic objects

(10:56) Shelves are a fixed set; books are not. Users keep adding books. Dynamic, user-defined values should be represented as Entities.

struct BookEntity: AppEntity, Identifiable {
    var id: UUID

    var displayRepresentation: DisplayRepresentation { "\(title)" }

    static var typeDisplayRepresentation: TypeDisplayRepresentation = "Book"

    static var defaultQuery = BookQuery()
}

Key points:

  • AppEntity makes BookEntity an object App Intents can pass around.
  • Identifiable provides a stable identifier the system saves into Shortcuts.
  • id: UUID is the book entity’s persistent identity.
  • displayRepresentation is the text users see in pickers and results.
  • typeDisplayRepresentation is the entity type name.
  • defaultQuery tells the system how to find the entity again from a saved identifier.

(12:29) The most basic query only needs to fetch entities by identifier.

struct BookQuery: EntityQuery {
    func entities(for identifiers: [UUID]) async throws -> [BookEntity] {
        identifiers.compactMap { identifier in
            Database.shared.book(for: identifier)
        }
    }
}

Key points:

  • EntityQuery is the system’s entry point for fetching Entities.
  • entities(for:) receives a set of persistent identifiers.
  • Database.shared.book(for:) looks up books in the app’s own database.
  • compactMap drops books that no longer exist or cannot be found.
  • The returned [BookEntity] is used by Intent parameters, Shortcuts pickers, or downstream actions.

(13:40) To let users select and search books in Shortcuts, upgrade the query to EntityStringQuery.

struct BookQuery: EntityStringQuery {
    func entities(for identifiers: [UUID]) async throws -> [BookEntity] {
        identifiers.compactMap { identifier in
            Database.shared.book(for: identifier)
        }
    }

    func suggestedEntities() async throws -> [BookEntity] {
        Database.shared.books
    }

    func entities(matching string: String) async throws -> [BookEntity] {
        Database.shared.books.filter { book in
            book.title.lowercased().contains(string.lowercased())
        }
    }
}

Key points:

  • EntityStringQuery adds text search on top of identifier lookup.
  • suggestedEntities() provides a default candidate list for the picker.
  • entities(matching:) returns entities for the user’s search string.
  • The example matches titles case-insensitively.
  • With many objects, return only common items in suggestedEntities() and put full search in entities(matching:).

Intents can return values and chain to another Intent

(15:11) Opening a book only reads existing data. Adding a book modifies the model, and a common next step is opening that book immediately. App Intents expresses this with return values and openIntent.

struct AddBook: AppIntent {
    static var title: LocalizedStringResource = "Add Book"

    @Parameter(title: "Title")
    var title: String

    @Parameter(title: "Author Name")
    var authorName: String?

    @Parameter(title: "Recommended By")
    var recommendedBy: String?

    func perform() async throws -> some IntentResult & ReturnsValue<BookEntity> & OpensIntent {
        guard var book = await BooksAPI.shared.findBooks(named: title, author: authorName).first else {
            throw Error.notFound
        }
        book.recommendedBy = recommendedBy
        Database.shared.add(book: book)

        return .result(
            value: book,
            openIntent: OpenBook(book: book)
        )
    }

    enum Error: Swift.Error, CustomLocalizedStringResourceConvertible {
        case notFound

        var localizedStringResource: LocalizedStringResource {
            switch self {
                case .notFound: return "Book Not Found"
            }
        }
    }
}

Key points:

  • Three @Parameter fields collect title, author, and recommender.
  • authorName and recommendedBy are optional parameters.
  • The return type combines IntentResult, ReturnsValue<BookEntity>, and OpensIntent.
  • BooksAPI.shared.findBooks looks up books with async/await.
  • Throws Error.notFound when no book is found.
  • CustomLocalizedStringResourceConvertible localizes error display.
  • Database.shared.add(book:) writes the result to the app database.
  • .result(value: book, openIntent: OpenBook(book: book)) returns the new book and provides a follow-up open action.

This design lets users compose freely in Shortcuts: add a book only, or turn on “Open When Run” to enter the app’s book page after adding.

Property Query turns app data into Shortcuts filters

(18:21) If an Entity only has a title, Shortcuts can only select and pass it. With @Property, users can read those fields in a Shortcut.

struct BookEntity: AppEntity, Identifiable {
    var id: UUID

    @Property(title: "Title")
    var title: String

    @Property(title: "Publishing Date")
    var datePublished: Date

    @Property(title: "Read Date")
    var dateRead: Date?

    var recommendedBy: String?

    var displayRepresentation: DisplayRepresentation { "\(title)" }

    static var typeDisplayRepresentation: TypeDisplayRepresentation = "Book"

    static var defaultQuery = BookQuery()

    init(id: UUID) {
        self.id = id
    }

    init(id: UUID, title: String) {
        self.id = id
        self.title = title
    }
}

Key points:

  • @Property(title: "Title") exposes the title to Shortcuts.
  • datePublished and dateRead let Shortcuts read date fields.
  • dateRead is optional for books not yet finished.
  • recommendedBy without @Property is not exposed as a queryable property.
  • displayRepresentation still controls how the entity itself is shown.
  • Initializers show Entities can be constructed for different scenarios.

(20:59) EntityPropertyQuery further turns properties into Shortcuts “Find” and “Filter” actions. The system owns editor UI; the app maps comparison conditions to its data layer.

struct BookQuery: EntityPropertyQuery {
    static var sortingOptions = SortingOptions {
        SortableBy(\BookEntity.$title)
        SortableBy(\BookEntity.$dateRead)
        SortableBy(\BookEntity.$datePublished)
    }

    static var properties = QueryProperties {
        Property(\BookEntity.$title) {
            EqualToComparator { NSPredicate(format: "title = %@", $0) }
            ContainsComparator { NSPredicate(format: "title CONTAINS %@", $0) }
        }
        Property(\BookEntity.$datePublished) {
            LessThanComparator { NSPredicate(format: "datePublished < %@", $0 as NSDate) }
            GreaterThanComparator { NSPredicate(format: "datePublished > %@", $0 as NSDate) }
        }
        Property(\BookEntity.$dateRead) {
            LessThanComparator { NSPredicate(format: "dateRead < %@", $0 as NSDate) }
            GreaterThanComparator { NSPredicate(format: "dateRead > %@", $0 as NSDate) }
        }
    }

    func entities(for identifiers: [UUID]) async throws -> [BookEntity] {
        identifiers.compactMap { identifier in
            Database.shared.book(for: identifier)
        }
    }

    func suggestedEntities() async throws -> [BookEntity] {
        Model.shared.library.books.map { BookEntity(id: $0.id, title: $0.title) }
    }

    func entities(matching string: String) async throws -> [BookEntity] {
        Database.shared.books.filter { book in
            book.title.lowercased().contains(string.lowercased())
        }
    }

    func entities(
        matching comparators: [NSPredicate],
        mode: ComparatorMode,
        sortedBy: [Sort<BookEntity>],
        limit: Int?
    ) async throws -> [BookEntity] {
        Database.shared.findBooks(matching: comparators, matchAll: mode == .and, sorts: sortedBy.map { (keyPath: $0.by, ascending: $0.order == .ascending) })
    }
}

Key points:

  • sortingOptions lists fields Shortcuts can sort by.
  • SortableBy(\BookEntity.$title) allows sorting by title.
  • QueryProperties declares which properties can be queried.
  • EqualToComparator and ContainsComparator map title comparisons.
  • Date properties use LessThanComparator and GreaterThanComparator.
  • The example uses NSPredicate as the mapping type because the data layer is Core Data.
  • entities(matching:mode:sortedBy:limit:) receives comparators, AND/OR mode, sorting, and limit.
  • Database.shared.findBooks(...) maps system-generated query conditions to the app’s own data query.

The concrete result: users can find books published in the early 20th century in Shortcuts, filter by author, export books read this year to CSV, or hand a decade of reading to a chart app.

Dialog, snippets, and clarification make Intents fit voice

(24:10) When Siri runs an Intent, users need to hear or see the result. ProvidesDialog gives text or spoken feedback about what the action did.

struct AddBook: AppIntent {
    static var title: LocalizedStringResource = "Add Book"

    @Parameter(title: "Title")
    var title: String

    @Parameter(title: "Author Name")
    var authorName: String?

    @Parameter(title: "Recommended By")
    var recommendedBy: String?

    func perform() async throws -> some IntentResult & ReturnsValue<BookEntity> & ProvidesDialog {
        guard var book = await BooksAPI.shared.findBooks(named: title, author: authorName).first else {
            throw Error.notFound
        }
        book.recommendedBy = recommendedBy
        Database.shared.add(book: book)

        return .result(
            value: book,
            dialog:"Added \(book) to Library!"
        )
    }

    enum Error: Swift.Error, CustomLocalizedStringResourceConvertible {
        case notFound

        var localizedStringResource: LocalizedStringResource {
            switch self {
                case .notFound: return "Book Not Found"
            }
        }
    }
}

Key points:

  • Return type adds ProvidesDialog.
  • .result(value:dialog:) returns both entity and feedback text.
  • dialog is spoken or shown in interfaces Siri or Shortcuts support.
  • Voice Intents should provide this feedback or users won’t know whether the action completed.

(24:25) Visual feedback uses a snippet. It is essentially a SwiftUI view returned as part of the result.

struct AddBook: AppIntent {
    static var title: LocalizedStringResource = "Add Book"

    @Parameter(title: "Title")
    var title: String

    @Parameter(title: "Author Name")
    var authorName: String?

    @Parameter(title: "Recommended By")
    var recommendedBy: String?

    func perform() async throws -> some IntentResult & ShowsSnippetView {
        guard var book = await BooksAPI.shared.findBooks(named: title, author: authorName).first else {
            throw Error.notFound
        }
        book.recommendedBy = recommendedBy
        Database.shared.add(book: book)

        return .result(value: book) {
            CoverView(book: book)
        }
    }

    enum Error: Swift.Error, CustomLocalizedStringResourceConvertible {
        case notFound

        var localizedStringResource: LocalizedStringResource {
            switch self {
                case .notFound: return "Book Not Found"
            }
        }
    }
}

Key points:

  • ShowsSnippetView means the Intent returns a visual snippet.
  • The trailing closure of .result(value:) { ... } supplies a SwiftUI view.
  • CoverView(book: book) can show the cover of the book just added.
  • This view is archived and sent to Siri or Shortcuts for display.

(24:50) When a parameter is missing or imprecise, throw requestValue so the system asks the user.

struct AddBook: AppIntent {
    static var title: LocalizedStringResource = "Add Book"

    @Parameter(title: "Title")
    var title: String

    @Parameter(title: "Author Name")
    var authorName: String?

    @Parameter(title: "Recommended By")
    var recommendedBy: String?

    func perform() async throws -> some IntentResult {
        let books = await BooksAPI.shared.findBooks(named: title, author: authorName)
        guard !books.isEmpty else {
            throw Error.notFound
        }
        if books.count > 1 && authorName == nil {
            throw $authorName.requestValue("Who wrote the book?")
        }

        return .result()
    }

    enum Error: Swift.Error, CustomLocalizedStringResourceConvertible {
        case notFound

        var localizedStringResource: LocalizedStringResource {
            switch self {
                case .notFound: return "Book Not Found"
            }
        }
    }
}

Key points:

  • BooksAPI.shared.findBooks looks up candidate books with existing parameters first.
  • guard !books.isEmpty handles finding nothing at all.
  • books.count > 1 && authorName == nil means the title alone does not distinguish results.
  • $authorName.requestValue("Who wrote the book?") asks the user for the author.
  • Thrown as an error; the system prompts the user and reruns the action with new parameters.

(25:22) When the system already has multiple candidates, use requestDisambiguation so the user picks from a list.

struct AddBook: AppIntent {
    static var title: LocalizedStringResource = "Add Book"

    @Parameter(title: "Title")
    var title: String

    @Parameter(title: "Author Name")
    var authorName: String?

    @Parameter(title: "Recommended By")
    var recommendedBy: String?

    func perform() async throws -> some IntentResult {
        let books = await BooksAPI.shared.findBooks(named: title, author: authorName)
        guard !books.isEmpty else {
            throw Error.notFound
        }
        if books.count > 1 {
            let chosenAuthor = try await $authorName.requestDisambiguation(among: books.map { $0.authorName }, dialog: "Which author?")
        }
        return .result()
    }

    enum Error: Swift.Error, CustomLocalizedStringResourceConvertible {
        case notFound

        var localizedStringResource: LocalizedStringResource {
            switch self {
                case .notFound: return "Book Not Found"
            }
        }
    }
}

Key points:

  • books.map { $0.authorName } extracts optional author names from candidates.
  • $authorName.requestDisambiguation(...) hands the list to the system for display.
  • dialog: "Which author?" supplies the prompt text.
  • try await waits for user selection; cancellation may throw.
  • chosenAuthor is the user’s choice for further lookup or write.

(26:26) Transactional actions should also confirm results. The buy-book order example shows result confirmation and a post-confirmation result snippet.

struct BuyBook: AppIntent {
    @Parameter(title: "Book")
    var book: BookEntity

    @Parameter(title: "Count")
    var count: Int

    static var title: LocalizedStringResource = "Buy Book"

    func perform() async throws -> some IntentResult & ShowsSnippetView & ProvidesDialog {
        let order = OrderEntity(book: book, count: count)
        try await requestConfirmation(output: .result(value: order, dialog: "Are you ready to order?") {
            OrderPreview(order: order)
        })

        return .result(value: order, dialog: "Thank you for your order!") {
            OrderConfirmation(order: order)
        }
    }
}

Key points:

  • book and count are the two parameters needed to place an order.
  • OrderEntity(book:count:) constructs the result awaiting confirmation.
  • requestConfirmation(output:) asks the user to confirm the order.
  • The confirmation request includes OrderPreview so users see a pre-order preview.
  • requestConfirmation throws if the user cancels.
  • The final .result returns the confirmed order, thank-you text, and confirmation view.

Choose in-app implementation or Extension

(27:09) App Intents has two deployment options. The simplest is directly in the app. No framework, no code duplication, no cross-process coordination. It also has higher memory limits and can do work unsuitable for Extensions, such as playing audio.

If openAppWhenRun is true, the app comes to the foreground. Otherwise the app launches in the background in a special mode without bringing up a scene, for better performance. The talk specifically recommends implementing scene support if you implement background App Intents in the app.

(28:05) Extensions are lighter because the process only handles App Intents and does not launch the full app. For Focus intents, Focus changes can run immediately in the Extension without the app being foregrounded. The cost is a new target, moving shared code into a framework, and coordinating app and Extension.

(28:52) App Intents metadata comes from static extraction at build time. Xcode generates a metadata file at build containing information the compiler reads from Intent, Entity, Query, and Parameter code. For extraction to work, App Intents types should live directly in the app target or Extension target, not in a framework. Related localized strings should also live in strings files in the same bundle.


Core Takeaways

1. Add a Siri entry for high-frequency pages

  • What to do: Turn pages users open every day into AppIntent, e.g. “open today’s tasks,” “open current order,” “open currently reading.”
  • Why it’s worth it: The talk’s OpenCurrentlyReading takes only a few lines to reach Shortcuts and appear in Siri and Spotlight via App Shortcut.
  • How to start: Define a struct conforming to AppIntent, call existing navigation in perform(); if it opens UI, set openAppWhenRun to true.

2. Expose in-app objects as Entities

  • What to do: Let users select your model objects in Shortcuts, such as projects, documents, contacts, playlists.
  • Why it’s worth it: BookEntity lets the system save object identifiers and recover objects via BookQuery when a Shortcut runs.
  • How to start: Conform your model or wrapper to AppEntity and Identifiable; provide displayRepresentation, typeDisplayRepresentation, and defaultQuery.

3. Build searchable parameter pickers for large lists

  • What to do: When users select objects, support searching the full database, not just fixed candidates.
  • Why it’s worth it: EntityStringQuery lets a book picker show suggestions first, then search the full library by user input.
  • How to start: Implement suggestedEntities() and entities(matching:) in Query; return common objects as suggestions and query the larger dataset in search.

4. Let Shortcuts compose filtering and sorting

  • What to do: Let users build “find all matching objects” actions in Shortcuts, e.g. overdue tasks, this year’s photos, this month’s orders for export.
  • Why it’s worth it: EntityPropertyQuery makes the system generate Find and Filter actions; the app only maps comparators to its database query.
  • How to start: Expose queryable fields with @Property, then implement QueryProperties, SortingOptions, and entities(matching:mode:sortedBy:limit:).

5. Add confirmation and feedback for voice actions

  • What to do: Let add, buy, delete, and send actions ask follow-up questions, disambiguate, confirm, and report results in Siri.
  • Why it’s worth it: The talk shows requestValue, requestDisambiguation, requestConfirmation, ProvidesDialog, and ShowsSnippetView, covering key voice interaction states.
  • How to start: Throw $parameter.requestValue() for missing parameters, $parameter.requestDisambiguation(...) for multiple candidates, requestConfirmation for transactions or destructive actions, then return results with dialog or SwiftUI snippet.

  • Implement App Shortcuts with App Intents — Covers App Shortcut implementation; good follow-up after mastering AppIntent to make actions appear automatically in Siri, Spotlight, and Shortcuts.
  • Design App Shortcuts — Covers phrase design, visuals, and information collection, complementing this article’s code focus.
  • Meet Focus filters — Shows App Intents in Focus filters for the same framework applied to system-level state changes.

Comments

GitHub Issues · utterances