Dive into App Intents
Description: Learn how you can make your app more discoverable and increase app engagement when you use the App Intents framework. We'll take you through the powerful capabilities of this Swift framework, explore the differences between App Intents and SiriKit Intents, and show you how you can expose your app's functionality to the system. We'll also share how you can build entities and queries to create rich App Shortcuts experiences. To learn more about App Intents, watch "Implement App Shortcuts with App Intents" and "Design App Shortcuts" from WWDC22.
Introduction
- new framework
- three key components:
Intents
,Entities
,App Shortcuts
- With App Shortcuts, everyone can use them via voice from Siri, they also appear in Spotlight
- Intents allow to build focus filters
- e.g., Calendar.app only shows work calendar when in work mode
- Users can invent entirely new features and capabilities
Intents and parameters
- Intent is a single piece of app functionality
- e.g., “make a new calendar event”, “open a particular screen”
- Performed manually or automatically
- either returns a
IntentResult
or throws anError
(possibly aIntentError
?) - three key pieces:
- the intent metadata - e.g., its title and description, shown to the user
- the intent parameters - all values can be customized by the user
- the intent
perform()
method - which is triggered when the user wants the intent to execute
- Example:
struct OpenCurrentlyReading: AppIntent {
static var title: LocalizedStringResource = "Open Currently Reading"
@MainActor // 👈🏻 ensure it's executed in the main thread
func perform() async throws -> some PerformResult { // 👈🏻
Navigator.shared.openShelf(.currentlyReading)
return .finished
}
static var openAppWhenRun: Bool = true
}
- This simple
OpenCurrentlyReading
definition automatically makes our app intent available in the following places:- Menu Bar
- Share Extensions
- Terminal
- AppleScript
- Home Screen
- Suggestions
- Lock Screen
- Shortcuts Widgets
- Quick Actions
- Voice (Siri)
- Apple Watch
- HomePod
- Automations
- Shortcuts App
- Keyboard
- Spotlight
- make your custom types conform to
AppEnum
to express that a custom type has a predefined, static set of valid values to display- can be used for types that have a known set of valid values
public enum Shelf: String, AppEnum {
case currentlyReading
case wantToRead
case read
static var typeDisplayName: LocalizedStringResource = "Shelf"
static var caseDisplayRepresentations: [Shelf: DisplayRepresentation] = [
.currentlyReading: "Currently Reading",
.wantToRead: "Want to Read",
.read: "Read",
]
}
- Use
@Parameter(title:)
to define your intent parameters
struct OpenShelf: AppIntent {
static var title: LocalizedStringResource = "Open Shelf"
@Parameter(title: "Shelf") // 👈🏻
var shelf: Shelf
@MainActor
func perform() async throws -> some PerformResult {
Navigator.shared.openShelf(shelf)
return .finished
}
static var parameterSummary: some ParameterSummary {
Summary("Open \(\.$shelf)")
}
static var openAppWhenRun: Bool = true
}
- Supported parameters types:
- Decimal
- Person
- Location
- URL
- Integer
- File
- Payment Method
- Rich Text
- Boolean
- Measurement
- Enumeration
- String
- Date
- Duration
- App Entity
- Currency
- Always provide a parameter
summary
for every intent you create, supportswhen
,otherwise
,switch
andcase
APIs - use static property
openAppWhenRun
to open app on running
Entities, queries, and results
- Entity contains identifier, display of representation and type name
- Any struct can conform to
AppEntity
:
struct BookEntity: AppEntity, Identifiable {
var id: UUID // 👈🏻 UUID is a good identifier type
var title: String
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(title: LocalizedStringResource(stringLiteral: title))
}
static var typeDisplayName: LocalizedStringResource = "Book"
static var defaultQuery = BookQuery()
}
Entity queries
- help the system find the entities your app defines and use them to resolve parameters.
StringQuery
andPropertyQuery
to look up entities- all queries support suggestions
- conform to
EntityQuery
on structs for queries - hook them up by adding
defaultQuery
to entity - conform to
EntityStringQuery
for string search, e.g. books - conform error to
CustomLocalizedStringResourceConvertible
- provide
ReturnsValue
if you want your shortcut return a result - adopt
OpensIntent
protocol in return type to show open button so users can select if app is opened or not
struct BookQuery: EntityQuery {
func entities(for identifiers: [UUID]) async throws -> [BookEntity] {
identifiers.compactMap { identifier in
Database.shared.book(for: identifier)
}
}
}
Properties, finding and filtering
- Property queries find entities on the properties within entity
- Three steps:
- Declare query properties
- Declare sorting options
- Implement
entities(matching:)
to run the search
- Supported query properties
- examples:
- less than and greater than for
Date
s - contains and equal to for
String
s
- less than and greater than for
- examples:
- Conform to
EntityPropertyQuery
with your comparators:
struct BookQuery: EntityPropertyQuery {
static var sortingOptions = SortingOptions {
SortableBy(\BookEntity.$title)
SortableBy(\BookEntity.$dateRead)
SortableBy(\BookEntity.$datePublished)
}
static var properties = EntityQueryProperties {
Property(keyPath: \BookEntity.title) {
EqualToComparator { NSPredicate(format: "title = %@", $0) }
ContainsComparator { NSPredicate(format: "title CONTAINS %@", $0) }
}
Property(keyPath: \BookEntity.datePublished) {
LessThanComparator { NSPredicate(format: "datePublished < %@", $0 as NSDate) }
GreaterThanComparator { NSPredicate(format: "datePublished > %@", $0 as NSDate) }
}
Property(keyPath: \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) })
}
}
User interactions
- Dialog
- spoken or textual response for intent
needsValueDialog
on@Parameter
for exampleValue
(something) result
- Snippet view
- visual equivalent of dialog
- lets you add a visual representation (a SwiftUI view) to the result of your intent
- return
.finished(dialog:view:)
as theIntentPerformResult
struct AddBook: AppIntent {
static var title: LocalizedStringResource = "Add Book"
@Parameter(title: "Title")
// ...
func perform() async throws -> some PerformResult {
// ..
return .finished(value: book) {
CoverView(book: book) // 👈🏻
}
}
enum Error: Swift.Error, CustomLocalizedStringResourceConvertible {
...
}
}
- Request Value - ask user for value via
requestValue(String)
- Disambiguation - let user choose via
requestDisambiguation(among:, dialog:)
- Confirmation
- request confirmation via
requestConfirmation(for:dialog:)
orrequestConfirmation(output: .result(value:dialog:))
- Last variant also supports showing a preview
- request confirmation via
struct AddBook: AppIntent {
static var title: LocalizedStringResource = "Add Book"
@Parameter(title: "Title")
// ...
func perform() async throws -> some PerformResult {
let books = // ... fetch books by reading @Parameter values
if books.count > 1 { // 👈🏻 too many matches! request disambiguation 👇🏻
let chosenAuthor = try await $authorName.requestDisambiguation(among: books.map { $0.authorName }, dialog: "Which author?")
}
return .finished
}
enum Error: Swift.Error, CustomLocalizedStringResourceConvertible {
...
}
}
Architecture and lifecycle
- In-app
- No need for a framework / duplicated code
- No cross-process coordination
- Higher memory limits
- Ability to play audio
- Run in foreground if you set
openAppWhenRun
- Implement multi-scene support for best performance
- Extension target
- Light-weight
- Best performance
- Focus filter intents, run immediately when Focus changes
- Create by choosing app intents extension template in Xcode
- Your code is the only source of truth
- Xcode extracts App Intent at build-time
- Compile AppIntents code directly into app / extension (not through package)
- Upgrading to App Intents
- keep using SiriKit intents for Widgets or Siri domains
- Others should upgrade