App Intents — это Swift-only фреймворк, который появился ещё в iOS 16 и пришёл на смену устаревшему SiriKit Intents. Главное отличие, ради которого Apple и затеяла переход: метаданные интента описываются прямо в Swift-коде. Без отдельного .intentdefinition-файла, без шага кодогенерации. Я лично помню, сколько боли приносила связка .intentdefinition + сгенерированный код в больших проектах — рефакторинг превращался в квест. С App Intents всё это в прошлом: рефакторинг, версионирование и тестирование сразу становятся привычным Swift-делом.
App Intent — это единица функциональности, которую вы выставляете системе: «добавить задачу», «начать тренировку», «отправить сообщение», «найти достопримечательность». После того как вы определили интент, система начинает использовать его повсеместно:
- Siri — вызов голосом, в том числе через App Shortcuts.
- Shortcuts.app — ручная сборка пользовательских автоматизаций.
- Spotlight — выполнение действий прямо из поиска.
- Widgets и Live Activities — интерактивные кнопки.
- Control Center и кнопка Action — на iPhone 15 Pro и новее.
- Apple Pencil Pro — squeeze-жест на iPad.
- Apple Intelligence — контекстные предложения и выполнение запросов.
В iOS 26 Apple усилила связку: теперь App Intents можно размещать в Swift-пакетах и статических библиотеках, переиспользуя их между iOS, macOS, watchOS и visionOS. Появились вычисляемые свойства сущностей через @ComputedProperty, расширения EnumerableEntityQuery и EntityPropertyQuery, а также интеграция с App Intent Domains для Apple Intelligence. Многого ждали — и оно наконец приехало.
Архитектура: интенты, сущности, запросы
Прежде чем писать код, давайте разберёмся в трёх базовых протоколах. Их всего три, и понимать их нужно не «вообще», а на уровне «когда какой брать».
AppIntent — описывает действие. Содержит метод perform(), который и выполняется системой.
AppEntity — описывает доменную модель (фильм, заметка, тренировка, маршрут), с которой работает интент. Сущности обязательно должны быть Identifiable.
EntityQuery — поставщик сущностей: умеет искать их по идентификаторам, по строке поиска или возвращать список «предложений» для UI Shortcuts.
Дополнительно используются AppEnum для перечислений с локализованным отображением, AppShortcutsProvider для регистрации App Shortcuts и IndexedEntity для семантической индексации в Spotlight. По мере чтения вы увидите, как они складываются вместе.
Шаг 1. Создаём первый App Intent
Допустим, мы пишем приложение для трекинга путешествий (вполне реальный кейс — у меня в чёрно-белом блокноте до сих пор валяется список «куда я хочу попасть»). Начнём с простого интента: «Добавить достопримечательность в избранное».
import AppIntents
struct AddFavoriteLandmarkIntent: AppIntent {
static let title: LocalizedStringResource = "Добавить в избранное"
static let description = IntentDescription(
"Добавляет указанную достопримечательность в список избранного.",
categoryName: "Путешествия"
)
@Parameter(title: "Достопримечательность")
var landmark: LandmarkEntity
static var parameterSummary: some ParameterSummary {
Summary("Добавить \(\.$landmark) в избранное")
}
func perform() async throws -> some IntentResult & ProvidesDialog {
try await FavoritesStore.shared.add(landmark.id)
return .result(
dialog: "Достопримечательность \(landmark.name) добавлена в избранное."
)
}
}
Что здесь происходит, по пунктам:
title — локализованный заголовок, который видит пользователь в Shortcuts.
description — длинное описание и категория, по которой интент группируется в галерее.
@Parameter — параметр интента. Может быть строкой, числом, датой, перечислением (AppEnum) или сущностью (AppEntity).
parameterSummary — шаблон, который Shortcuts показывает в редакторе («Добавить $X в избранное»).
perform() — асинхронная функция, выполняющая действие. Возвращаемый тип определяет, что увидит пользователь: голосовой ответ Siri (ProvidesDialog), сниппет (ShowsSnippetView) или просто факт успешного выполнения.
Шаг 2. Описываем доменную модель через AppEntity
Чтобы интент мог принимать параметром «достопримечательность», нам нужно научить систему этой модели. Без этого Shortcuts просто не будет знать, что подсунуть пользователю в выпадашке. Создаём LandmarkEntity:
import AppIntents
struct LandmarkEntity: AppEntity {
var id: Int { landmark.id }
@ComputedProperty
var name: String { landmark.name }
@ComputedProperty
var summary: String { landmark.summary }
@ComputedProperty
var country: String { landmark.country }
let landmark: Landmark
static let typeDisplayRepresentation = TypeDisplayRepresentation(
name: "Достопримечательность"
)
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(
title: "\(name)",
subtitle: "\(country)",
image: .init(named: landmark.imageName)
)
}
static let defaultQuery = LandmarkEntityQuery()
}
Ключевые требования к AppEntity:
- Тип должен быть
Identifiable — у каждой сущности уникальный id.
typeDisplayRepresentation описывает тип в целом («Достопримечательность», «Заметка», «Тренировка»).
displayRepresentation — как показывать конкретный экземпляр в UI Shortcuts, в виджете или в галерее Siri.
defaultQuery — запрос, который система использует для поиска сущностей.
Аннотация @ComputedProperty, которая появилась в iOS 26, позволяет выставить вычисляемые поля сущности так, чтобы система понимала их семантику и могла использовать в фильтрах EntityPropertyQuery. На практике это и есть то, ради чего стоит переехать на iOS 26 SDK при первой возможности.
Шаг 3. Реализуем EntityQuery
Без запроса система просто не сможет «найти» сущность по идентификатору, который пользователь когда-то сохранил в Shortcut. Самая простая реализация выглядит так:
struct LandmarkEntityQuery: EntityQuery {
@Dependency var modelData: ModelData
func entities(for identifiers: [LandmarkEntity.ID]) async throws -> [LandmarkEntity] {
modelData
.landmarks(for: identifiers)
.map(LandmarkEntity.init)
}
func suggestedEntities() async throws -> [LandmarkEntity] {
modelData.featuredLandmarks.map(LandmarkEntity.init)
}
}
Метод entities(for:) вызывается тогда, когда система загружает Shortcut с сохранённым ID — например, при запуске пользовательской автоматизации. suggestedEntities() возвращает «по умолчанию хорошие» сущности — их Shortcuts.app покажет, когда пользователь только настраивает шорткат и ещё не выбрал ничего конкретного.
Для полнотекстового поиска по сущностям расширьте запрос протоколом EntityStringQuery:
extension LandmarkEntityQuery: EntityStringQuery {
func entities(matching string: String) async throws -> [LandmarkEntity] {
modelData.search(query: string).map(LandmarkEntity.init)
}
}
А для запросов по конкретному свойству (например, «найти достопримечательности страны Япония») — используйте EntityPropertyQuery. Это новинка iOS 18, расширенная в iOS 26:
extension LandmarkEntityQuery: EntityPropertyQuery {
static var properties = QueryProperties {
Property(\LandmarkEntity.country) {
EqualToComparator { country in
NSPredicate(format: "country == %@", country)
}
}
}
static var sortingOptions = SortingOptions {
SortableBy(\LandmarkEntity.name)
}
func entities(
matching comparators: [NSPredicate],
mode: ComparatorMode,
sortedBy: [Sort<LandmarkEntity>],
limit: Int?
) async throws -> [LandmarkEntity] {
modelData.query(predicates: comparators, sort: sortedBy, limit: limit)
.map(LandmarkEntity.init)
}
}
Шаг 4. Регистрируем App Shortcuts
App Shortcuts — это «преднастроенные» сценарии, которые система знает о вашем приложении ещё до того, как пользователь открыл его впервые. Их обнаруживают Siri, Spotlight и Shortcuts.app автоматически. На приложение допускается до 10 таких шорткатов (это лимит, и он жёсткий — не пытайтесь обойти).
struct TravelTrackingAppShortcuts: AppShortcutsProvider {
static var shortcutTileColor: ShortcutTileColor = .lightBlue
@AppShortcutsBuilder
static var appShortcuts: [AppShortcut] {
AppShortcut(
intent: AddFavoriteLandmarkIntent(),
phrases: [
"Добавить достопримечательность в \(.applicationName)",
"Сохранить \(\.$landmark) в \(.applicationName)"
],
shortTitle: "В избранное",
systemImageName: "star.circle"
)
AppShortcut(
intent: NavigateToLandmarkIntent(),
phrases: [
"Проложить маршрут в \(.applicationName)",
"Маршрут до \(\.$landmark) в \(.applicationName)"
],
shortTitle: "Маршрут",
systemImageName: "map.circle"
)
}
}
Несколько важных правил, которые легко упустить:
- Каждая фраза обязана содержать
\(.applicationName) — это имя приложения, как его произнесёт пользователь.
- Параметры в фразах синтаксически выглядят как
\(\.$landmark) — система автоматически предложит выбрать сущность через ваш EntityQuery.suggestedEntities().
- Регистрация автоматическая: достаточно добавить тип, реализующий
AppShortcutsProvider, в основной таргет приложения.
- Локализуйте через файл
AppShortcuts.strings для каждого языка, который поддерживаете. Иначе на «нероднóм» языке Siri просто не услышит вас.
Шаг 5. Параметры с диалогом и подтверждением
Иногда интент требует от пользователя дополнительных данных или подтверждения опасного действия. Для этого есть три удобных метода: requestValue, requestConfirmation и requestDisambiguation.
struct DeleteLandmarkIntent: AppIntent {
static let title: LocalizedStringResource = "Удалить достопримечательность"
static let isDiscoverable: Bool = true
@Parameter(title: "Достопримечательность")
var landmark: LandmarkEntity
@Parameter(title: "Подтверждение", default: false)
var confirm: Bool
func perform() async throws -> some IntentResult {
if !confirm {
try await requestConfirmation(
result: .result(
dialog: "Удалить «\(landmark.name)»? Действие необратимо."
)
)
}
try await FavoritesStore.shared.remove(landmark.id)
return .result()
}
}
Шаг 6. Интерактивные виджеты на App Intents
Начиная с iOS 17, виджеты могут содержать кнопки и тогглы, которые исполняют App Intents без открытия приложения. На мой взгляд, это самый частый и самый практичный сценарий использования интентов в реальных проектах. Пользователь нажимает на кнопку «лайк» прямо в виджете — и всё, действие выполнено.
struct ToggleFavoriteIntent: AppIntent {
static let title: LocalizedStringResource = "Переключить избранное"
@Parameter(title: "Достопримечательность")
var landmark: LandmarkEntity
func perform() async throws -> some IntentResult {
try await FavoritesStore.shared.toggle(landmark.id)
return .result()
}
}
struct FavoriteWidgetView: View {
let landmark: Landmark
var body: some View {
VStack(alignment: .leading) {
Text(landmark.name).font(.headline)
Spacer()
Button(intent: ToggleFavoriteIntent(landmark: .init(landmark: landmark))) {
Label(
landmark.isFavorite ? "В избранном" : "Добавить",
systemImage: landmark.isFavorite ? "star.fill" : "star"
)
}
.tint(.yellow)
}
.padding()
}
}
Ключевые моменты для виджетов (по этим граблям многие проходят, включая меня):
- Виджет и приложение должны разделять состояние через App Group:
UserDefaults(suiteName: ...) или базу данных в общей папке.
- После выполнения интента вызовите
WidgetCenter.shared.reloadTimelines(ofKind:), чтобы виджет обновил UI.
- Кнопки на основе
AppIntent не открывают приложение — это и есть смысл интерактивности. Если открытие нужно — используйте OpensIntent.
Apple Intelligence: домены, схемы и семантический поиск
В iOS 18 Apple ввела App Intent Domains — типизированные «схемы» интентов, которые понимает Apple Intelligence. Если ваш интент соответствует одному из доменов (фотографии, заметки, плеер, почта, путешествия и так далее), Siri может вызвать его в правильном контексте, даже если пользователь сформулирует запрос свободно. Без названия приложения, без точных команд — просто человеческой речью.
import AppIntents
@AssistantIntent(schema: .photos.openAsset)
struct OpenLandmarkPhotoIntent: AppIntent {
static let title: LocalizedStringResource = "Открыть фото"
@Parameter(title: "Фото")
var target: LandmarkPhotoEntity
func perform() async throws -> some IntentResult {
await Router.shared.openPhoto(id: target.id)
return .result()
}
}
Аннотация @AssistantIntent(schema:) говорит системе: «этот интент — реализация стандартной операции "открыть актив"». И теперь Apple Intelligence может маршрутизировать запросы вроде «покажи фото горы Фудзи» прямо в ваш интент, не требуя от пользователя называть приложение.
IndexedEntity: семантический поиск
Вторая половина магии — индексация ваших сущностей в Spotlight. Реализуйте IndexedEntity и периодически обновляйте индекс:
extension LandmarkEntity: IndexedEntity {
var attributeSet: CSSearchableItemAttributeSet {
let attrs = CSSearchableItemAttributeSet(contentType: .item)
attrs.title = name
attrs.contentDescription = summary
attrs.keywords = [country, "достопримечательность", "путешествие"]
attrs.thumbnailURL = landmark.thumbnailURL
return attrs
}
}
@MainActor
func reindexLandmarks() async {
let entities = ModelData.shared.allLandmarks.map(LandmarkEntity.init)
try? await CSSearchableIndex.default()
.indexAppEntities(entities)
}
После индексации сущности попадают в семантический индекс на устройстве. Apple Intelligence использует его для подсказок и исполнения запросов, при этом данные никогда не покидают устройство — это, пожалуй, важнейшая гарантия приватности всей системы.
Тестирование App Intents
Хорошая новость: App Intents — это обычные Swift-структуры, и тестировать их можно как любую другую бизнес-логику. Используйте Swift Testing (мы уже разбирали его в одной из предыдущих статей):
import Testing
@testable import TravelTracker
@Suite("AddFavoriteLandmarkIntent")
struct AddFavoriteLandmarkIntentTests {
@Test("Добавляет сущность в хранилище")
func addsToStore() async throws {
let store = MockFavoritesStore()
FavoritesStore.shared = store
let landmark = Landmark.preview(id: 42, name: "Эльбрус")
var intent = AddFavoriteLandmarkIntent()
intent.landmark = .init(landmark: landmark)
_ = try await intent.perform()
#expect(store.savedIds == [42])
}
}
Для интеграционных проверок используйте Shortcuts.app: добавьте свой шорткат в редактор, настройте параметры и запустите. Если интент в редакторе не появился — проверьте сразу три вещи: содержит ли фраза \(.applicationName), добавлен ли AppShortcutsProvider в основной таргет, и не вылетает ли исключение в perform() ещё до вашего breakpoint.
Производительность и ограничения
- Время выполнения.
perform() должен завершаться за разумное время. Для длительных операций используйте OpensIntent, чтобы открыть приложение.
- Главный поток. UI-операции выполняйте на
@MainActor; в Swift 6 компилятор сам проследит за изоляцией.
- App Group. Если интент исполняется из виджета — общие данные обязаны быть в App Group. Без вариантов.
- Лимит шорткатов. Не более 10
AppShortcut на приложение. Стройте остальные сценарии через интенты, доступные в Shortcuts.app, но без авто-регистрации.
- Локализация. Фразы должны быть локализованы — Siri распознаёт только те, которые есть в
AppShortcuts.strings для текущего языка устройства.
Чек-лист готовности к Apple Intelligence
- Описать ключевые действия приложения как
AppIntent.
- Выставить доменные модели как
AppEntity с реализацией EntityQuery.
- Зарегистрировать 5–10 самых частых сценариев в
AppShortcutsProvider с локализованными фразами.
- Добавить
@AssistantIntent для интентов, соответствующих стандартным доменам (фотографии, плеер, заметки, путешествия, почта и т. д.).
- Реализовать
IndexedEntity и обновлять индекс при изменении данных.
- Покрыть интенты юнит-тестами и обязательно проверить их в Shortcuts.app.
- Убедиться, что виджеты используют
Button(intent:) и Toggle(isOn:intent:) вместо deeplink-ссылок.
Часто задаваемые вопросы
Чем App Intents отличаются от старого SiriKit?
SiriKit Intents работал на отдельном .intentdefinition-файле и кодогенерации, требовал расширения Intents Extension и поддерживал ограниченный набор доменов. App Intents — Swift-only фреймворк: интенты живут прямо в коде, поддерживают любые доменные модели, виджеты, Spotlight, кнопку Action и Apple Intelligence. С 2026 года SiriKit считается унаследованным API, и новые проекты должны строиться исключительно на App Intents.
Можно ли вызвать App Intent программно изнутри приложения?
Да, и это чаще, чем кажется. Создайте экземпляр интента, заполните параметры и вызовите perform() напрямую: let result = try await MyIntent().perform(). Очень удобно для повторного использования бизнес-логики — например, в обработчиках deep link, в push-уведомлениях или прямо из SwiftUI-вью.
Как локализовать фразы App Shortcut?
Создайте файл AppShortcuts.strings для каждого поддерживаемого языка и поместите его в основной таргет приложения. Ключи берутся из phrases, значения — это переведённые варианты с теми же интерполяциями (${applicationName}, ${landmark}). Без локализации Siri не сможет распознать команду на любом языке, отличном от языка разработки. Тут компромиссов нет.
Работают ли App Intents на macOS, watchOS и visionOS?
Да, фреймворк кросс-платформенный. С iOS 26 интенты можно класть в Swift Package и переиспользовать между iPhone, iPad, Mac, Apple Watch и Vision Pro. Часть возможностей зависит от платформы: Apple Pencil Pro доступен только на iPad, Action Button — на iPhone 15 Pro и новее, домены Apple Intelligence — на устройствах, где работает сам Apple Intelligence.
Покидают ли данные сущностей устройство?
Нет. При использовании IndexedEntity и App Intent Domains данные индексируются локально в Semantic Index. Apple Intelligence обрабатывает запросы на устройстве, а если требуется большая модель — использует Private Cloud Compute с криптографически проверяемой приватностью. Apple не имеет доступа к содержимому индекса, и третьи стороны его тоже не получают.
Что дальше
App Intents — это инвестиция, которая окупается на каждом следующем релизе iOS. Новые точки интеграции (Control Center, Action Button, Apple Pencil Pro, Apple Intelligence) автоматически получают доступ к интентам, которые вы уже написали. Так что, честно говоря, лучшее время добавить App Intents в проект — это вчера. Второе лучшее — сегодня. Начните с одного-двух самых частых действий, выставьте их через AppShortcutsProvider, добавьте интерактивные кнопки в виджет — и приложение сразу станет «нативным гражданином» iOS 26.
В следующих статьях мы разберём связку App Intents + Live Activities для управления текущими событиями голосом, а также напишем продакшн-уровень App Intent для Apple Watch с синхронизацией состояния через SwiftData. Stay tuned.