Using App Intents with iOS 16

Use your iPhone with your voice: the dream of every iOS user

Giulio Caggegi
9 min readNov 25, 2022

Recently Apple introduced a new framework, App Intents. The goal of this framework is simplify the code to interact with Siri and improve the user experience with your own iPhone. In this post I’ll explain how to approach this framework, what are the main heroes and how they interact among them.

Intents and App Shortcut

Firstly, we’ll analyze the main actors of these framework, Intent and Shortcut. An Intent is simply an action that iOS can execute. In the Apple documentation the intent description is:

An app intent includes the code you need to perform an action, and describes the data you require from the system

In the Apple way, generally an intent is a piece of code that Siri can execute. We can combine more intents like a puzzle to create more complex activities or a sensible sequence: this is the App Shortcut job! The App Shortcut is famous for iOS users; through “Shortcut” app, users can create a sequence of executable actions and associate a phrase to start them with Siri. The new App Intents framework gives the possibility to developers to create and build an App Shortcut without no longer need the setup of the user. The shortcut can be used in Shortcut app, but can’t be edited by the user.

The framework gives the possibility to set also a series of sentences that the user can pronounces to activate the Shortcut with Siri.

App Entity

The AppEntity is the domain model of our shortcuts. Let’s see the code:

/// This is the model used with `AppIntents` framework.
@available(iOS 16.0, *)
struct PokemonAppEntity: AppEntity, Identifiable {

/// The `TypeDisplayRepresentation` of the entity.
static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Pokemon")

/// The needed `defaultQuery` in order to conform `PokemonAppEntity` to `AppEntity`.
static var defaultQuery: PKMNIntentsQuery = PKMNIntentsQuery()

/// The unique `String` used to identify the entity.
var id: String

/// The `@Property` for the name.
@Property(title: "Name")
var name: String

/// The `DisplayRepresentation` type planned by the `AppEntity`.
/// A type that describes the user interface presentation of a custom type.
var displayRepresentation: DisplayRepresentation {
return DisplayRepresentation(
title: "\(name)",
subtitle: nil,
image: .init(data: AppAsset.pokeball.image.pngData())
)
}

// MARK: - Init

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

// MARK: - Extensions

@available(iOS 16.0, *)
extension PokemonAppEntity: Equatable {
static func == (lhs: PokemonAppEntity, rhs: PokemonAppEntity) -> Bool {
return lhs.id == rhs.id
}
}

@available(iOS 16.0, *)
extension PokemonAppEntity: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}

The PokemonAppEntity struct needs to conform the AppEntity and Identifiable protocols. The AppEntity expects:

  • A static TypeDisplayRepresentation, that is a title used in different places to describe the entity.
  • A DisplayRepresentation, that is a struct with a title, an optional subtitle and an image, used in the Shortcut App to show an instance of the entity. Notes that the image must be a static image, never dynamic.
  • A static defaultQuery, an interface for locating entities using their identifiers.
  • We added also a @Property name String, which exposes to the system the String associated property.

In the extension we made the entity conform to Hashable and Equatable protocols.

EntityQuery

As seen, the AppEntity protocol expects a defaultQuery, in order to allow Siri and Shortcut app to fetch the AppEntity. So we must implement a class conform to EntityQuery protocol. In this way, Siri and App Shortcuts can fetch exactly the entity the user is referring to.

For example, a user may have chosen a Pokemon from a list when tapping on a parameter that accepts Pokemon. The ID of that Pokemon is now hardcoded into the Shortcut.

import AppIntents

/// The `EntityPropertyQuery` of the `PokemonAppEntity`.
@available(iOS 16.0, *)
struct PKMNIntentsQuery: EntityQuery {
// MARK: - Entities Methods

/// Find an entity by his ID.
/// For example a user may have chosen a Pokemon from a list when tapping on a parameter that accepts Pokemon. The ID of that Pokemon is now hardcoded into the `Shortcut`.
/// - Parameter identifiers: The list of IDs for the Pokemons that the user selected.
/// - Returns: The list of `[PokemonAppEntity]` found.
func entities(for identifiers: [String]) async throws -> [PokemonAppEntity] {
let pokemons = try await IntentsLogic.FetchData.Pokemons.execute()
return pokemons.filter { identifiers.contains($0.id) }
}

/// Returns all Pokemon. This is what populates the list when you tap on a parameter that accepts a Pokemon.
func suggestedEntities() async throws -> [PokemonAppEntity] {
try await IntentsLogic.FetchData.Pokemons.execute()
}
}

The protocol expects also to implement the suggestedEntities, which returns the initial results shown when a list of options backed by this query is presented. All the entities' methods are asynchronous.

But…we have a @Property var in our AppEntity. Can we use this property to query an AppEntity? The answer is “yes, we can”! We must conform our PKMNIntentsQuery to EntityPropertyQuery. To do this, we must add some properties to our struct:

  • A list of EntityQueryProperties
  • A list of SortingOptions
  • A new entities method
/// The `EntityPropertyQuery` of the `PokemonAppEntity`.
@available(iOS 16.0, *)
struct PKMNIntentsQuery: EntityPropertyQuery {

// MARK: - Computed Properties

/// A type that provides the properties to include in a property-matched query.
static var properties = EntityQueryProperties<PokemonAppEntity, String> {
Property(\.$name) {
EqualToComparator { $0 }
ContainsComparator { $0 }
}
}

/// The sorting options provided by the `EntityPropertyQuery` protocol.
static var sortingOptions = SortingOptions {
SortableBy(\.$name)
}

// MARK: - Entities Methods

/// Find an entity by his ID.
/// For example a user may have chosen a Pokemon from a list when tapping on a parameter that accepts Pokemon. The ID of that Pokemon is now hardcoded into the `Shortcut`.
/// - Parameter identifiers: The list of IDs for the Pokemons that the user selected.
/// - Returns: The list of `[PokemonAppEntity]` found.
func entities(for identifiers: [String]) async throws -> [PokemonAppEntity] {
let pokemons = try await IntentsLogic.FetchData.Pokemons.execute()
return pokemons.filter { identifiers.contains($0.id) }
}

func entities(matching comparators: [String], mode: ComparatorMode, sortedBy: [Sort<PokemonAppEntity>], limit: Int?) async throws -> [PokemonAppEntity] {
guard !comparators.isEmpty, let name = comparators.first else {
return try await IntentsLogic.FetchData.Pokemons.execute()
}
return try await IntentsLogic.FetchData.PokemonByQuery.execute(query: name)
}

/// Returns all Pokemon. This is what populates the list when you tap on a parameter that accepts a Pokemon.
func suggestedEntities() async throws -> [PokemonAppEntity] {
try await IntentsLogic.FetchData.Pokemons.execute()
}
}

The EntityQueryProperties has a list of EntityQueryComparator that specifies a particular query comparison that can be made against a EntityQueryProperty. The EntityQueryComparator, like EqualToComparator or ContainsComparator are called at runtime with “value” being the user-supplied value. In our case the comparator is a simple String, but can be a NSPredicate or a URLQueryItem or whatever.

The comparators are the input for the new entities method, and can be used to filter the AppEntity.

If all is right, you can find in the Shortcut app a new shortcut action like this:

App Intent

Now we have our AppEntity and our EntityQuery; it’s time to see the App Intent. Let’s see the code:

@available(iOS 16, *)
/// This is the real implementation of `AppIntent`. Is the action to show the information about a `PokemonAppEntity`.
struct CatchPokemon: AppIntent {
/// The title of the action in the `Shortcut` app.
static var title: LocalizedStringResource = PKMNString.AppIntents.title

/// The description of the action.
static var description: IntentDescription = IntentDescription(stringLiteral: String(localized: PKMNString.AppIntents.description))

/// Authentication policies to apply when running an app intent.
static var authenticationPolicy = IntentAuthenticationPolicy.alwaysAllowed

/// Wheter or not the app should be open or not.
static var openAppWhenRun = false

/// The dynamic lookup parameter.
@Parameter(title: "Pokemon")
var pokemon: PokemonAppEntity?

@MainActor
func perform() async throws -> some ReturnsValue<PokemonAppEntity> {
let pokemonToCatch: PokemonAppEntity
if let pokemon = pokemon {
pokemonToCatch = pokemon
} else {
pokemonToCatch = try await $pokemon.requestDisambiguation(among: try await IntentsLogic.FetchData.Pokemons.execute(), dialog: IntentDialog(stringLiteral: String(localized: PKMNString.AppIntents.dialog)))
}
return .result(value: pokemonToCatch, view: PokemonInformation(pokemon: pokemonToCatch))
}
}

This simple AppIntent shows in a View the information of the selected Pokemon.

Our struct must be conforming the AppIntent protocol, so we need to implement some static properties:

  • The LocalizedStringResource title, that is the title of the action.
  • The IntentDescription description, that is the description of the action.

Optionally, we can specify other static properties, such as the IntentAuthenticationPolicy or the boolean openAppWhenRun.

The IntentAuthenticationPolicy describes the authentication policy to use when an AppIntent running. So for example we can set the policy to alwaysAllowed if the AppIntent can be run even on a locked device; the others possible values are

  • requiresAuthentication when the AppIntent requires the user to authenticate in the local device or in a paired device such as an Apple Watch.
  • requiresLocalDeviceAuthentication when the AppIntent requires the user to authenticate only in the local device.

The boolean openAppWhenRun instead describes when the main app should be open or not.

To determine which Pokemon to show, we’ve added a pokemon property to our struct. This property is configured with the @Parameter property wrapper.

The next step is to implement the perform method. When our App Intent is executed, we may already know what Pokemon the user intends to see, or we may not have that information yet(for example if the user use the Intent in the Shortcut app), so we need to support both scenarios.

The App Intents framework provides Disambiguation to ask users follow-up questions. A disambiguation is triggered by calling requestDisambiguation(among:dialog:) on our Pokemon property wrapper, known as $pokemon.

The perform method returns with an IntentResult. In the example we use the method to return a value, that the user can use in a Shortcut sequence; the same result is used to present a simple SwiftUI View with the related value’s information. As you can see, we have multiple solutions and possibilities:

App Shortcut

The last step is to create an AppShortcut in order to execute automatically the AppIntent. Let’s see the code:

@available(iOS 16.0, *)
/// The `AppShortcutsProvider`.
struct PokemonShortcutsProvider: AppShortcutsProvider {
@AppShortcutsBuilder
static var appShortcuts: [AppShortcut] {
AppShortcut(
intent: OpenPokemon(),
phrases: ["Catch a Pokemon in \(.applicationName)"]
)
}
}

The array of phrases contains the possible sentences(also localized phrases, as we will see after), that the users can pronounce to execute the intent. Notes that all the phrases must contain the applicationName app token.

Now, it’s possible to execute the intent pronouncing a phrase, or find it in the Shortcut app or in the Siri suggestion flow:

Localization

A special chapter is dedicated to the localization of the AppShortcut. Notes that all the parameters and the presented intent dialogs must be localized with the new LocalizedStringResource struct. For example, we can ask Siri to read a phrase, presenting an IntentDialog. If we want to localize the string, we can do it in this way:

IntentDialog(stringLiteral: String(localized: LocalizedStringResource(...)))

The init of the LocalizedStringResource allows us to specify the table and the key for the localization.

We can localize also the phrases of the array of the AppShortcut . To make this, we must create a new strings file exactly with the AppShortcuts.strings name;

Notes that all the strings used for the AppShortcuts must contain the application name surrounded in this way:

${applicationName}

In our AppShortcutsProvider we will able to pass directly the key of the localized string in this way:

@available(iOS 16.0, *)
/// The `AppShortcutsProvider`.
struct PokemonShortcutsProvider: AppShortcutsProvider {
@AppShortcutsBuilder
static var appShortcuts: [AppShortcut] {
AppShortcut(
intent: OpenPokemon(),
phrases: ["shortcut.show.pokemons"]
)
}
}

Next Step

Now you know the step-by-step instructions to implement AppShortcut and AppIntent. You can explore the configurations, parameters and possibilities that this framework offers. Enjoy yourself with AppIntents framework!

--

--