Привет! Меня зовут Настя Ищенко. Я — iOS-разработчик в KTS.
Эта статья поможет узнать, что такое App Intents и как их использовать для создания сложных сценариев, которые расширят возможности вашего приложения. А еще я расскажу об обновлениях фреймворка App Intents, представленных на WWDC24.
Оглавление:
В этом году Apple уделили особое внимание развитию ИИ и расширению возможностей настройки системы. Повторяющиеся задачи или сложные рабочие процессы — все это можно будет выполнять с помощью Controls, Action button и Shortcuts, а также с помощью Siri после внедрения Apple Intelligence.
Как пользователь, я нашла обновления, представленные на WWDC24, очень удобными. Но как разработчик сразу представила, сколько сложных взаимодействий потребуется для новых элементов управления — звучит непросто. Так ли это? На самом деле — нет, с фреймворком App Intents все гораздо проще.
Что такое App Intents
App Intents — это «мостик» между системой и вашим приложением. Фреймворк помогает системе понять, какой функционал доступен в вашем приложении, и интегрировать их в работу устройства. Можно сказать, что App Intents — это основа взаимодействия с Siri и элементами управления.
Сценариев использования App Intents довольно много:
Spotlight
Action button
Apple pencil
Виджеты
Controls
Siri
Shortcuts
Focus Filters
Camera capture
Accessibility actions
Работа с live activities при помощи App Intents в background для простых задач, не требующих дополнительных действий в приложении (при помощи Siri/Shothcuts и т. д.).
App Intents позволяют пользователям выполнять нужные функции вашего приложения, даже когда оно закрыто. Например, пользователи могут запустить определенное действие через Siri, выбрать команду в Shortcuts или воспользоваться функцией на панели управления (в виджетах). Благодаря App Intents вам не нужно отдельно настраивать каждое взаимодействие — достаточно один раз задать нужные функции, и они станут доступны пользователям через Siri, команды, панель управления и т.д.
Как создать AppIntent?
При создании любого AppIntents вы столкнетесь с двумя основными протоколами — Entities и Intents. Intents выполняют действия. Entities — это объект, с которым выполняется действие.
Легко понять смысл этих двух элементов фреймворка поможет простая аналогия: Intents — это глаголы, Entities — существительные. Их связывает в единое «предложение» App Shortcuts – программа, которая помогает быстро запускать ключевые функции приложения прямо из системы. Эти команды заранее настраиваются разработчиком, чтобы упростить доступ к наиболее часто используемым функциям.
Для создания базового Intent, соответствующего протоколу AppIntent, необходимо создать объект, соответствующий трем критериям:
Реализует имя для intent — static var title. Этот заголовок отобразится у пользователя в Shortcuts, Spotlight и т. д.
Реализует описание того, что он делает — static var description. Необходимо для того, чтобы система (например, Siri) могла распознать действие по описанию пользователя.
Реализует функцию для выполнения этого действия — func perform() async throws -> some IntentResult. Это необходимо для выполнения самого действия AppIntent.
Это все, что потребуется для реализации базового AppIntent. Далее, к примеру, можно добавить ParameterSummary для настройки параметров действия или соответствие протоколу WidgetConfigurationIntent, чтобы предоставить пользователю возможность добавить виджет с командой на экран устройства.
Пример Intent
Пример Intent
struct GetTotalExpenseIntent: AppIntent {
static var title = LocalizedStringResource("Получить общую сумму расходов")
static var description = IntentDescription("Показывает общую сумму расходов")
func perform() async throws -> some ReturnsValue<Double> & ProvidesDialog {
let store = ExpenseStore.shared
let amount = store.expenses.reduce(Double(0)) { (result, element) in
result + element.value
}
return .result(
value: amount,
dialog: .init("Вы потратили \(amount, specifier: "%.0f").")
)
}
}
Intent может содержать параметры, являющиеся базовыми типами, либо параметры AppEntity. Протокол AppEntity позволяет использовать кастомный тип данных для работы с AppIntent. Если ваш тип является «тяжелым», то можно создать тип, соответствующий AppEntity и имеющий ссылку на необходимые классы.
Пример AppEntity
Пример AppEntity
struct ExampleEntity: AppEntity {
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Крутая App Entity"
let id: Int
let value: String
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(
title: LocalizedStringResource(
stringLiteral: value
)
)
}
static var defaultQuery = ExampleEntityQuery()
}
struct ExampleEntityQuery: EntityQuery {
func entities(for identifiers: [Int]) async throws -> [ExampleEntity] {
// логика по получению нужных ExampleEntity
}
}
Согласно протоколу AppEntity, ваш тип должен иметь:
ID — уникальный идентификатор Entity;
typeDisplayRepresentation — общий заголовок для элемента;
displayRepresentation — значение конкретного элемента;
query — тип, который преобразует запрос к Entity в саму Entity.
При выборе значения для параметра EntityQuery должен отвечать на два вопроса:
Какие Entity есть? Для ответа на этот вопрос query должен быть подписан на один из доступных протоколов, например EnumerableEntityQuery и реализовать allEntities() async throws -> [Entity], в котором и будет осуществляться поиск. Устройство покажет список вариантов, из которых можно выбрать.
Когда будет выбрана Entity, будет сохранен ее ID, при запуске Intent сохраненный ID будет отправлен, после чего Query должен ответить на второй вопрос:
Какая Entity имеет этот ID? Отвечая на этот вопрос query возвращает Entity, и Intent получает саму Entity, а не ее ID.
Можно использовать и другой тип поиска — например, по любому другому полю AppEntity (не по ID). Самое важное — ответить на поставленные к Query вопросы.
Кроме AppEntity, фреймворк AppIntents предоставляет протокол AppEnum. Его стоит использовать, если ваш Intent предоставляет возможность выбора из определенных вариантов параметра, которые можно реализовать с помощью enum. Пример AppEnum будет приведен в практической части статьи.
Когда вы создали AppIntent, вы можете предоставить возможность использовать созданное действие в Spotlight, Shortcuts и Siri. С этим вам помогут AppShortcuts.
Что такое Shortcuts
Shortcuts как составляющая часть фреймворка AppIntents
Для добавления действия в Spotlight и Siri необходимо создать App Shortcut. App Shortcut — это обертка над AppIntent, которая ее «подсвечивает» как важную функцию приложения. После создания App Shortcut разные приложения (например Spotlight, Action button, Apple pencil и другие) будут предлагать пользователю выполнить это действие.
Для создания Shortcuts необходимо создать тип, подписанный на протокол AppShortcutsProvider со статичным свойством AppShortcuts, которое будет описывать список Shortcuts приложения.
AppShortcuts оборачивает экземпляр класса Intent. Это означает, что при необходимости можно заранее заполнить свойства Intent и затем передать экземпляр в инициализатор AppShortcuts (если для команды можно заранее настроить входные параметры Intent):
import AppIntents
struct ShortcutsProvider: AppShortcutsProvider {
static var appShortcuts: [AppShortcut] {
AppShortcut(
intent: GetTotalExpenseIntent(), //можно использовать инициализатор с заполненными параметрами
phrases: ["Получить общую сумму расходов в \(.applicationName)"],
shortTitle: "Получить общую сумму расходов",
systemImageName: "dollarsign.circle.fill"
)
AppShortcut(
intent: AddExpenseIntent(), //можно использовать инициализатор с заполненными параметрами
phrases: ["Добавить расход в \(.applicationName)"],
shortTitle: "Добавить расход",
systemImageName: "dollarsign.circle.fill"
)
AppShortcut(
intent: GetExpensesChartIntent(), //можно использовать инициализатор с заполненными параметрами
phrases: ["Посмотреть статистику по тратам в \(.applicationName)"],
shortTitle: "Посмотреть статистику по тратам",
systemImageName: "dollarsign.circle.fill"
)
}
}
AppShortcuts в качестве параметра инициализатор также принимает фразы, которые будут триггерить действие — например, отображать в поиске Spotlight по ключевым словам, и визуальную информацию — название и иконку Shortcuts, которые будут отображаться пользователю.
Фреймворк AppIntents автоматически определяет AppShortcutsProvider и осуществляет регистрацию, поэтому App Shortcuts становятся доступны, как только приложение будет установлено. Пользователю не нужно ничего настраивать.
Shortcuts как приложение
Shortcuts (aka «Команды» в русской версии ОС) — это системное приложение, которое предоставляет пользователям интерфейс работы с командами других установленных приложений.
Возможность создавать команды через App Intents была представлена на WWDC22 вместе с iOS 16. Однако Shortcuts не сразу стали популярными среди пользователей. Этому было две причины:
Философия компании. Предполагалось, что каждое приложение должно было предлагать лишь несколько команд. Это ограничивало возможности для создания сценариев.
Ограниченные возможности возможности комбинации команд из разных приложений и работы с файлами через App Intents. В результате Shortcuts не мог использовать весь потенциал для создания многошаговых и межприложенческих сценариев.
Если коротко — в этом году оба этих недостатка были устранены, что однозначно сыграет позитивную роль относительно популярности AppIntents. Подробнее на обоих пунктах остановимся далее в статье.
Компания представила Apple Intelligence — усовершенствованную технологию искусственного интеллекта, которая сделала Siri действительно умным помощником. После недавних обновлений Siri и App Intents могут работать вместе. Пользователи могут легко выполнять повседневные задачи, не открывая приложения и даже не глядя на экран — достаточно одной голосовой команды.
Теперь каждое приложение может предложить пользователю гибкие команды и доступ ко всем функциям, которые разработчики считают полезными. Поэтому главная задача разработчика — предоставить доступ к нужным функциям и гибкие настройки для их использования в Shortcuts, чтобы пользователи могли полноценно автоматизировать свои действия и объединять возможности нескольких приложений в одном сценарии.
Для эффективного использования App Intents важно учитывать несколько нюансов:
Итоговый Shortcut должен быть читаемым и логичным предложением. Если ваш Shortcut принимает входные параметры, используйте ParameterSummary, чтобы команда была понятна для пользователя.
App Intents может соответствовать разным протоколам, в зависимости от задач. Самый простой — OpenIntent. AppIntent, соответствующие этому протоколу, открывают приложение при активации действия. Выбирайте протоколы, которые лучше всего подходят под цели вашего продукта. Документация
App Intents можно настроить так, чтобы он работал без открытия приложения. Это особенно полезно для взаимодействия с Siri и выполнения быстрых команд в фоне. Важно учитывать: если у вас русскоязычная аудитория, обновленная Siri пока не доступна в России.
Расширение гайдлайнов для App Intents. Ранее App Intents охватывали только ключевые функции, полезные вне основного интерфейса. В iOS 18 Apple расширила гайдлайны, позволяя включать любые функции вашего приложения. С учетом роста количества доступных команд, их можно приоритизировать для более удобного использования.
Гайдлайн от Apple по созданию App Intents
Начать создание App Intents стоит с базовых действий. Чаще всего это создание, изменение, удаление, получение и другие команды по работе с информацией.
Следует избегать создания нескольких App Intents для выполнения одной задачи. Пример плохих App Intents:
Чтобы избежать создания таких действий, стоит использовать настраиваемый параметр:
Избегайте создания App Intents для запуска действий с определенными элементами пользовательского интерфейса. Описание App Intent должно представлять задачу, которая будет выполнена, а не способ ее выполнения.
Дополнительные замечания от Apple:
Как было написано ранее, формулировки App Intents должны быть читаемыми предложениями — это стоит учитывать при использовании параметров. Это важно для понимания пользователем функционала действия, выполняемого App Intents. Если вы используете параметры, то нужно использовать ParameterSummary.
ParameterSummary — выражение на естественном языке, описывающее, что сделает Intent, включая значения всех параметров. При нажатии на плейсхолдер параметра, вызывается query, который выдает возможные значения параметра. Выбранный параметр заменяет плейсхолдер. Благодаря этому описание Shortcut приобретает вид логичного выражения, содержащего в себя описание того, что делает Intent.
Пример
struct AddExpenseIntent: AppIntent {
…
@Parameter(title: "Категория")
var category: Category?
@Parameter(title: "Сумма")
var amount: Double?
static var parameterSummary: some ParameterSummary {
Summary("Добавить трату \(\.$amount)р. в категорию: \(\.$category)")
}
…
Параметры могут быть либо опциональными — их необязательно задавать перед запуском действия, либо обязательными.
Если действие имеет бинарное состояние, например, как в случае фонарика (включено и выключено), параметр App Intent должен по умолчанию принимать значение Toggle. Это позволит пользователям использовать App Intents без необходимости каждый раз выбирать состояние (например, включение или отключение фонарика).
Открытие приложения при работе с App Intents
Ранее считалось, что лучше избегать App Intent, которые открывают приложение без явной необходимости. В iOS 18 открытие вашего приложения с App Intent стало стандартным поведением, позволяющим показать пользователям, какие изменения команда внесла в приложение.
Существует две основные причины для открытия вашего приложения в рамках использования App Intents.
Первая — если ваш App Intent по своей сути предназначен для открытия определенного экрана.
Вторая — если ваш App Intent завершается изменением пользовательского интерфейса приложения или отображением результатов поиска.
Пользователь может отключить открытие приложения при воспроизведении команды при помощи переключателя Open when run, или в целом выключить отображение интерфейса команды, переключив тогл Show when run. Это наиболее полезно при создании сложных сценариев команд.
Практика
С примерами, которые я рассмотрю ниже, можно ознакомиться в репозитории.
Рассмотрим на примере приложения, для которого будем создавать App Intents и Shortcuts.
Это маленькое приложение на SUi, отслеживающее сумму трат. Данные хранятся в UserDefaults.
Главный экран приложения:
Сначала создадим простой App Intent:
Создание App Intent
import AppIntents
struct GetTotalExpenseIntent: AppIntent {
static var title = LocalizedStringResource("Получить общую сумму расходов")
static var description = IntentDescription("Показывает общую сумму расходов")
func perform() async throws -> some ReturnsValue<Double> & ProvidesDialog {
let store = ExpenseStore.shared
let amount = store.expenses.reduce(Double(0)) { (result, element) in
result + element.value
}
return .result(
value: amount,
dialog: .init("Вы потратили \(amount, specifier: "%.0f").")
)
}
}
Как понятно из title и description, созданный AppIntent возвращает сумму потраченных средств.
Метод perform(), который вызывается при запуске Intent, всегда возаращает .result. Тип возвращаемого значения в нашем случае — ReturnsValue<Double> & ProvidesDialog. Это означает, что Intent возвращает значение Double, а также отображает диалоговое окно:
После добавления кода GetTotalExpenseIntent наш новый Intent не будет видно в Shortcuts и Spotlight. Чтобы это исправить, необходимо добавить объект, соответствующий протоколу AppShortcutsProvider.
Добавление объекта
import AppIntents
struct ShortcutsProvider: AppShortcutsProvider {
static var appShortcuts: [AppShortcut] {
AppShortcut(
intent: GetTotalExpenseIntent(),
phrases: ["Получить общую сумму расходов в \(.applicationName)"],
shortTitle: "Получить общую сумму расходов",
systemImageName: "dollarsign.circle.fill"
)
}
}
Поскольку GetTotalExpenseIntent возвращает Double, мы можем использовать его в любом месте в коде. Например, давайте добавим Intent, который при запуске будет отображать диаграмму с главного экрана, и используем его для получения общей суммы трат (totalExpenses).
Добавление Intent
import AppIntents
import SwiftUI
struct GetExpensesChartIntent: AppIntent {
static var title = LocalizedStringResource("Посмотреть статистику по тратам")
static var description = IntentDescription("Демонстрирует статистику по тратам")
@MainActor
func perform() async throws -> some ProvidesDialog & ShowsSnippetView {
let totalExpenses = (try? await GetTotalExpenseIntent().perform().value) ?? 0
return .result(
dialog: "Всего вы потратили \(totalExpenses, specifier: "%.0f")",
view: ChartView(
expensesModel: ExpenseStore.shared.chartExpensesData,
totalExpense: totalExpenses,
showTotalExpense: false
)
)
}
}
Запуск GetExpensesChart:
Intent отображает SwiftUI View, поскольку метод perform() имеет тип возвращаемого значения ShowsSnippetView. Важно не забыть при использовании ShowsSnippetView добавить import SwiftUI, иначе ничего не заведется с не самой очевидной ошибкой:
Важно: если результат вашего метода perform является ShowsSnippetView, то необходимо добавлять к нему атрибут MainActor.
Теперь создадим Intent, который будет добавлять трату определенного размера в выбранную пользователем категорию:
Создание Intent
import AppIntents
import SwiftUI
struct AddExpenseIntent: AppIntent {
static var title = LocalizedStringResource("Добавить расход")
static var description = IntentDescription("Сохраняет информацию о расходе")
@Parameter(title: "Категория")
var category: Category?
@Parameter(title: "Сумма")
var amount: Double?
static var parameterSummary: some ParameterSummary {
Summary("Добавить трату размером: \(\.$amount) в категорию: \(\.$category)")
}
init() {}
init(category: Category?, amount: Double?) {
self.category = category
self.amount = amount
}
@MainActor
func perform() async throws -> some ProvidesDialog & ShowsSnippetView {
if amount == nil {
amount = try await $amount.requestValue(.init(stringLiteral: "Сколько денег вы потратили?"))
}
if category == nil {
category = try await $category.requestValue(.init(stringLiteral: "На что вы их потратили?"))
}
ExpenseStore.shared.addExpense(category: category!, amount: amount!)
return .result(
dialog: "Сохранили трату \(amount!, specifier: "%.0f") для категории: \(category!)",
view: ChartView(
expensesModel: ExpenseStore.shared.chartExpensesData,
totalExpense: ExpenseStore.shared.totalExpense,
showTotalExpense: false
)
)
}
}
Из кода видно, что мы используем два параметра — category и amount. Category — это изначально следующий enum:
enum
import SwiftUI
enum Category: String, CaseIterable {
case food = "Еда"
case health = "Здоровье"
case clothes = "Одежда"
case other = "Другое"
var title: String {
switch self {
case .clothes: return "Еда"
case .health: return "Здоровье"
case .food: return "Одежда"
case .other: return "Другое"
}
}
var color: Color {
switch self {
case .clothes: return Color.clothes
case .health: return Color.health
case .food: return Color.food
case .other: return Color.gray
}
}
}
Для того, чтобы использовать его в AppIntent, мы должны добавить соответствие протоколу AppEnum:
Добавление соответствия
extension Category: AppEnum {
static var typeDisplayRepresentation: TypeDisplayRepresentation = .init(name: "Категория")
static var typeDisplayName: LocalizedStringResource = "Категория"
static var caseDisplayRepresentations: [Category : DisplayRepresentation] = [
.clothes: .init(stringLiteral: "Одежда"),
.food: .init(stringLiteral: "Еда"),
.health: .init(stringLiteral: "Здоровье"),
.other: .init(stringLiteral: "Другое")
]
}
Проверки на nil значений в методе perform нового Intent необходимы для того, чтобы при отсутствии значения этих параметров они были запрошены у пользователя. А parameterSummary используется для отображения читаемого текста при создании Shortcut. Например:
Запуск AddExpenseIntent:
На гифке сначала запускается Shortcut с незаполненными параметрами, а потом Shortcut с предзаполненными параметрами «1000» и «Одежда» (см. последний скрин).
Готово. Теперь пользователь может добавлять траты и просматривать их через приложение Shortcuts, Spotlight или прямо с главного экрана (если добавит нужный Shortcut на главный экран).
Вы можете использовать ваши AppIntent в коде просто для выполнения действия, например, использовать как действие по кнопке. Ради эксперимента добавим кнопку на главный экран:
Button(intent: AddExpenseIntent(category: .other, amount: 500)) {
Text("Добавить трату 500 в категорию: \(Category.other.rawValue)")
.multilineTextAlignment(.center)
.foregroundStyle(Color.black.opacity(0.5))
.padding(10)
}
Основным преимуществом фреймворка AppIntents является то, что он позволяет использовать одну и ту же логику для разных фичей.
Допустим, для создания виджета просмотра трат, достаточно было бы добавить соответствие протоколу WidgetConfigurationIntent созданному до этого GetExpensesChartIntent - при этом даже не нужно реализовывать какие-либо дополнительные свойства и функции:
extension GetExpensesChartIntent: WidgetConfigurationIntent {}
Новые фишки AppIntents, которые могут быть полезны
Взаимодействие различных Intents
Как я уже писала, пользователь может создавать сложные сценарии в приложении Shortcuts. Действия в этих сценариях не обязательно должны быть из одного приложения.
Чтобы обеспечить взаимодействие вашего приложения с другими, например с системными заметками, необходимо реализовать протокол Transferable. Тогда результат выполнения ваших App Intents можно будет экспортировать в разных форматах: png, rtf и других.
Если же вы хотите принимать в качестве параметра какой-либо из предоставляемых Transferable типов контента, то необходимо в атрибуте соответствующего параметра указать поддерживаемые типы контента:
В дальнейшем в методе perform вы сможете найти наиболее предпочтительный тип контента:
Описанный выше механизм полезен, если результат выполнения Intent не является файлом, но может быть преобразован в него. Если результат Intent уже сам по себе файл, то для экспорта этого результата стоит выбрать FileEntity.
Благодаря FileEntity Siri и Shortcuts могут предоставить защищенный доступ к вашему файлу другим приложениям, позволяя им получать доступ непосредственно к самому файлу.
Пример FileEntity:
Благодаря обновлениям AppIntents, связанным с файлами, появилась возможность полноценного взаимодействия между приложениями с помощью Siri и Shortcuts.
Универсальные ссылки
Теперь вы можете выразить ваши AppEntity, AppEnum и AppIntent в виде URLRepresentation. Это позволит Siri и Shortcuts обращаться к ним, как к ссылке на конкретный контент, и открывать URL адреса или совершать другие действия с полученным URL.
Появились:
Подписка на протокол URLRepresentableIntent не потребует обязательной имплементации каких-либо свойств или методов, если он взаимодействует с App Entity, для которого реализовано свойство urlRepresentation протокола URLRepresentableEntity.
Обновления для разработчиков
UnionValue
UnionValue используется, когда у вас есть параметр или свойство, которое может быть представлено одним из множества типов. По своей сути, UnionValue — это макрос (что такое макросы, читайте в моей статье). Он позволяет использовать enum, атрибутом которого является, в качестве параметра AppIntent.
Важно, чтобы каждый кейс enum имел только одно связанное значение. Помимо этого, каждое связанное значение должно быть уникальным. Используйте UnionValue, когда у вас есть несколько типов и вы хотите принять один из них. Грубо говоря, UnionValue — это логический элемент «или».
Generated titles
При работе с Xcode 16.0 вам больше не нужно указывать заголовок для свойств AppEntity или параметров AppIntent, если они совпадают с названием переменной. Xcode будет генерировать строку заголовка для вас на основе имени свойства.
До:
После:
Framework improvements
Раньше все типы App Intent должны были находиться в одном модуле. Использовать AppEntity в одном из ваших фреймворков было нельзя. AppIntent и AppEntity должны были находиться в одном фреймворке.
В Xcode 16 это ограничение было снято. Теперь можно определять сущности приложения в отдельном фреймворке и ссылаться на них как в основном приложении, так и в extension targets.
Вывод
Фреймворк App Intents анонсировали достаточно давно, но до недавнего времени он не имел широкого распространения, особенно в России. Основная причина заключалась в том, что App Intents приложения могли включать только его основной функционал. Это ограничивало возможности по настройке действий и созданию всех сценариев Shortcuts, полезных конкретному пользователю.
В этом году Apple сделала акцент на расширении списка App Intents в приложениях, компания расширила возможности взаимодействия между ними и представила Siri с поддержкой нового ИИ. Возможность создавать сложные Shortcuts привлекла меня как пользователя — планирую попробовать их для повседневных задач. Связка Siri с ИИ выглядит интересно, хотя ее запуск пока ожидается только для англоязычных пользователей, и неизвестно, когда (и в каком объеме) она станет доступна в России.
Само создание App Intents не должно занять много времени и сил: процесс выглядит достаточно простым, особенно радует возможность шеринга кода между разными типами Intents, что помогает использовать одну логику для взаимодействия с разными фичами системы: Siri, Shortcuts, Widgets и так далее.
Наши другие статьи про разработку на iOS:
Источники:
WWDC
Полезные статьи: