App Intents в Swift: интеграция с Apple Intelligence, Siri и Spotlight в iOS 26

Полное руководство по фреймворку App Intents в iOS 26: создание интентов, AppEntity, EntityQuery, App Shortcuts, интерактивные виджеты и интеграция с Apple Intelligence и семантическим поиском Spotlight. Готовые примеры кода на Swift 6.2 и Xcode 26.

App Intents iOS 26: Siri и Apple Intelligence

Окей, давайте честно: в 2026 году App Intents перестали быть «опциональной интеграцией». Это уже не «было бы неплохо добавить», а обязательный интерфейс между вашим приложением и системой. Apple Intelligence, обновлённая Siri, Spotlight с семантическим поиском, интерактивные виджеты, кнопка Action, Control Center — всё это работает поверх App Intents. И если приложение не выставляет интенты наружу, оно становится буквально «невидимым» в iOS 26: пользователь не сможет ни попросить Siri что-то сделать, ни найти ваши данные через Spotlight, ни поместить их в виджет.

В этом гайде мы пройдём фреймворк App Intents с нуля до продвинутых сценариев — от создания интентов с параметрами и описания сущностей через AppEntity, до регистрации App Shortcuts, индексации для семантического поиска (IndexedEntity) и интеграции с App Intent Domains для Apple Intelligence. Все примеры — на Swift 6.2, всё проверено в Xcode 26.

Что такое App Intents и зачем они нужны в iOS 26

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. Многого ждали — и оно наконец приехало.

Архитектура: интенты, сущности, запросы

Прежде чем писать код, давайте разберёмся в трёх базовых протоколах. Их всего три, и понимать их нужно не «вообще», а на уровне «когда какой брать».

  1. AppIntent — описывает действие. Содержит метод perform(), который и выполняется системой.
  2. AppEntity — описывает доменную модель (фильм, заметка, тренировка, маршрут), с которой работает интент. Сущности обязательно должны быть Identifiable.
  3. 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

  1. Описать ключевые действия приложения как AppIntent.
  2. Выставить доменные модели как AppEntity с реализацией EntityQuery.
  3. Зарегистрировать 5–10 самых частых сценариев в AppShortcutsProvider с локализованными фразами.
  4. Добавить @AssistantIntent для интентов, соответствующих стандартным доменам (фотографии, плеер, заметки, путешествия, почта и т. д.).
  5. Реализовать IndexedEntity и обновлять индекс при изменении данных.
  6. Покрыть интенты юнит-тестами и обязательно проверить их в Shortcuts.app.
  7. Убедиться, что виджеты используют 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.

Об авторе Priya Raghavan

Priya spent six years at Instacart building the iOS shopper app, where she led the migration from UIKit to SwiftUI across 80+ screens and cut crash-free sessions from 99.2% to 99.87%. Before that, she was a contractor at a Bay Area design studio shipping App Store apps for two Fortune 500 retail clients. She focuses on practical SwiftUI architecture - what holds up when you have 12 engineers committing to the same codebase, not just toy MVVM examples. Her recent work involves The Composable Architecture, Swift concurrency migration audits, and reducing main-thread hangs on older devices like the iPhone XR that enterprise fleets still ship. Priya runs a small consultancy in Oakland and occasionally speaks at try! Swift NYC. She has been writing Swift since the Objective-C bridging days of 2015.