Introduction
three key components:
Intents,Entities,App ShortcutsWith 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
IntentResultor 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
OpenCurrentlyReadingdefinition 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
AppEnumto express that a custom type has a predefined, static set of valid values to displaycan 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
Currency
Always provide a parameter
summaryfor every intent you create, supportswhen,otherwise,switchandcaseAPIsuse static property
openAppWhenRunto 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.
StringQueryandPropertyQueryto look up entitiesall queries support suggestions
conform to
EntityQueryon structs for querieshook them up by adding
defaultQueryto entityconform to
EntityStringQueryfor string search, e.g. booksconform error to
CustomLocalizedStringResourceConvertibleprovide
ReturnsValueif you want your shortcut return a resultadopt
OpensIntentprotocol 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
examples:
less than and greater than for
Datescontains and equal to for
Strings
Conform to
EntityPropertyQuerywith 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
spoken or textual response for intent
needsValueDialogon@Parameterfor exampleValue(something) result
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
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
openAppWhenRunImplement 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
