App Intents у Swift для iOS 26: повний посібник

Повний посібник з App Intents у Swift для iOS 26: створення намірів, AppEntity, EntityQuery, App Shortcuts та інтеграція з Apple Intelligence через AssistantSchemas.

App Intents у Swift iOS 26: Посібник

Оновлено: 29 травня 2026

App Intents — це декларативний фреймворк Swift, який дозволяє вашому застосунку експонувати дії та дані для Siri, Spotlight, Shortcuts, Apple Intelligence та системних поверхонь iOS 26. Замість того щоб реєструвати команди Siri через старі Intent-файли SiriKit, ви описуєте свої наміри звичайним кодом Swift: один тип, що відповідає протоколу AppIntent, працює одночасно як ярлик у Shortcuts, як швидка дія у Spotlight та як виклик, який Apple Intelligence може запропонувати в потрібну мить. У цьому посібнику я проведу вас від першого perform() до інтеграції з AppEntity, EntityQuery та новим AssistantSchemas у iOS 26.

  • App Intents це сучасна заміна SiriKit для iOS 16 і пізніше; у iOS 26 фреймворк отримав AssistantSchemas, які роблять наміри видимими для Apple Intelligence.
  • Будь-який тип, що відповідає AppIntent, автоматично з'являється у Shortcuts, Spotlight та підтримує голосові виклики без додаткової конфігурації .intentdefinition.
  • AppEntity та EntityQuery експонують доменні моделі (нотатки, замовлення, тренування) системі, дозволяючи їй посилатися на них у параметрах і результатах.
  • Метод perform() повертає some IntentResult, тобто типізовану відповідь, яка може містити діалог, представлення або значення для chaining наступних дій.
  • App Shortcuts, оголошені через AppShortcutsProvider, з'являються в Spotlight одразу після першого запуску і не потребують ручного донату до системи.
  • Тестувати App Intents найзручніше через perform() напряму у Swift Testing, бо це звичайна async-функція без UI-залежностей.

Що таке App Intents і чим вони відрізняються від SiriKit

App Intents це Swift-only фреймворк, представлений у iOS 16 і доопрацьований у кожному наступному релізі аж до iOS 26. Він замінює стару пару SiriKit + Intents Extension і прибирає необхідність у файлах .intentdefinition, окремому таргеті розширення та згенерованому Objective-C коді. Ви оголошуєте намір як звичайну структуру Swift, а компілятор робить решту: реєструє метадані, генерує опис параметрів, експонує тип у Shortcuts і Spotlight.

З історичної перспективи: SiriKit з'явився у 2016 році з фіксованим набором доменів (повідомлення, платежі, тренування), і додавати власні дії можна було лише через INIntent-розширення з обмеженим API. Цей шлях успадковано з Objective-C епохи й він зберігся переважно для ресторанних замовлень та CarPlay. App Intents натомість дозволяють будь-якому застосунку експонувати довільні дії без обмежень доменів. У iOS 26 фреймворк отримав AssistantSchemas: це типи, які повідомляють Apple Intelligence, що ваш намір реалізує одну зі стандартних дій (наприклад, "знайти нотатку" або "створити нагадування"), і модель може викликати його в системних запитах.

Практично це означає таке. Один тип, який відповідає AppIntent, одразу доступний у п'яти місцях: Shortcuts, Spotlight (як швидка дія), Action Button, Siri та Apple Intelligence. Жодних дублювань, жодного NSUserActivity-моста, жодних окремих donate-викликів. Чесно кажучи, після кількох років жонглювання SiriKit-розширеннями це відчувається як ковток свіжого повітря.

Як створити перший App Intent на Swift

Мінімальний намір це структура з трьома обов'язковими елементами: статичним title, типом, що повертає perform(), і самою реалізацією. Усе інше (параметри, описи, опційний діалог) додається в міру потреби. Розглянемо приклад наміру, який відкриває нову нотатку в застосунку для записів.

import AppIntents

struct CreateNoteIntent: AppIntent {
    static let title: LocalizedStringResource = "Створити нотатку"
    static let description = IntentDescription("Створює нову порожню нотатку та відкриває її.")
    static let openAppWhenRun = true

    @MainActor
    func perform() async throws -> some IntentResult & ProvidesDialog {
        let note = await NoteStore.shared.createEmpty()
        await NavigationCoordinator.shared.open(note: note)
        return .result(dialog: "Нову нотатку створено.")
    }
}

Зверніть увагу на чотири деталі. По-перше, title це LocalizedStringResource, що автоматично підтягує переклад зі String Catalog. По-друге, openAppWhenRun = true повідомляє системі, що дія потребує вашого UI у форграунді. Для дій, які працюють тихо в фоні (наприклад, "додати елемент до списку покупок"), залиште значення за замовчуванням false. По-третє, @MainActor на perform() гарантує, що ми не сходимо з головного потоку при роботі з UI-станом. Це особливо важливо, коли ви вже мігрували на Approachable Concurrency у Swift 6.2, де MainActor застосовується за замовчуванням.

По-четверте, повернений тип some IntentResult & ProvidesDialog це композиція протоколів. IntentResult обов'язковий контракт, ProvidesDialog додає голосову відповідь Siri, ReturnsValue<T> дозволяє повернути значення для chaining (про це нижче), а OpensIntent переадресовує до іншого наміру. Усі вони комбінуються через &.

Параметри, dialog та resolution

Більшість намірів приймають вхідні дані. Параметр оголошується через property wrapper @Parameter, що генерує метадані для UI Shortcuts і автоматичних промптів Siri. Розглянемо намір, що позначає завдання виконаним:

struct CompleteTaskIntent: AppIntent {
    static let title: LocalizedStringResource = "Позначити завдання виконаним"

    @Parameter(title: "Завдання", description: "Завдання, яке потрібно позначити.")
    var task: TaskEntity

    @Parameter(title: "Нотатка", description: "Опціональний коментар.", default: "")
    var note: String

    @MainActor
    func perform() async throws -> some IntentResult & ProvidesDialog {
        try await TaskStore.shared.complete(task.id, note: note)
        return .result(dialog: "Завдання \(task.title) позначено виконаним.")
    }

    static var parameterSummary: some ParameterSummary {
        Summary("Позначити \(\.$task) виконаним") {
            \.$note
        }
    }
}

parameterSummary це DSL, що описує, як намір виглядає в редакторі Shortcuts. Параметри, перелічені поза замиканням, ставляться в основний рядок; параметри в замиканні з'являються в розгорнутому стані. Без parameterSummary система згенерує запасний варіант, але він рідко читається природно.

Коли користувач викликає намір голосом і не вказує параметра, Siri використовує resolution, щоб дізнатися значення. Для більшості випадків достатньо позначити параметр як @Parameter(requestValueDialog:), але для складніших сценаріїв реалізуйте requestValue вручну, повертаючи IntentDialog з підказкою. Якщо значення є, але потребує дезамбігуації, поверніть .disambiguation(among:dialog:) зі списком кандидатів, і Siri прочитає їх голосом і дочекається вибору.

AppEntity та EntityQuery: експонування доменних моделей

Параметри типу String або Int працюють для простих випадків, але реальні застосунки оперують власними моделями: нотатки, замовлення, тренування, контакти. Щоб система могла посилатися на ці об'єкти (наприклад, "позначити оце завдання виконаним" з контекстного меню), модель має відповідати протоколу AppEntity.

struct TaskEntity: AppEntity {
    static let typeDisplayRepresentation: TypeDisplayRepresentation = "Завдання"
    static let defaultQuery = TaskQuery()

    let id: UUID
    let title: String
    let isCompleted: Bool

    var displayRepresentation: DisplayRepresentation {
        DisplayRepresentation(title: "\(title)",
                              subtitle: isCompleted ? "Виконано" : "Активне")
    }
}

struct TaskQuery: EntityQuery {
    func entities(for identifiers: [UUID]) async throws -> [TaskEntity] {
        await TaskStore.shared.tasks(ids: identifiers)
    }

    func suggestedEntities() async throws -> [TaskEntity] {
        await TaskStore.shared.recent(limit: 10)
    }
}

Три речі тут варті уваги. По-перше, id має бути стабільним між запусками. UUID, який ви зберігаєте в SwiftData або Core Data, ідеальний. Якщо ви використовуєте SwiftData, погляньте, як наслідування моделей у SwiftData впливає на форму ідентифікаторів: підкласи успадковують primary key батька.

По-друге, suggestedEntities() повертає список, який система використовує як стартові пропозиції в Shortcuts. Тримайте його коротким (5–15 елементів), бо це не повний дамп вашої бази, а саме пропозиції.

По-третє, для масштабованих сховищ реалізуйте EntityStringQuery або EntityPropertyQuery на додачу до базової EntityQuery. Перший підтримує текстовий пошук ("знайди завдання про обід"), другий — фільтрацію за властивостями ("активні завдання за останні 7 днів"). Apple Intelligence у iOS 26 особливо активно використовує EntityPropertyQuery, бо це структуроване API, яке модель може викликати з обмеженою свободою.

App Shortcuts і інтеграція зі Spotlight

Звичайний AppIntent доступний у Shortcuts, але користувач має його туди додати руками. AppShortcutsProvider вирішує цю проблему: список ярликів, оголошений у вашому застосунку, з'являється в Spotlight і пропозиціях Siri одразу після першого запуску, без жодного donate-виклику.

struct NotesShortcuts: AppShortcutsProvider {
    static var appShortcuts: [AppShortcut] {
        AppShortcut(
            intent: CreateNoteIntent(),
            phrases: [
                "Створити нотатку в \(.applicationName)",
                "Нова нотатка \(.applicationName)"
            ],
            shortTitle: "Нова нотатка",
            systemImageName: "square.and.pencil"
        )
        AppShortcut(
            intent: SearchNotesIntent(),
            phrases: [
                "Знайти нотатку в \(.applicationName)",
                "Шукати в \(.applicationName) \(\.$query)"
            ],
            shortTitle: "Пошук нотаток",
            systemImageName: "magnifyingglass"
        )
    }
}

Фрази мають містити \(.applicationName), бо це системна вимога, що захищає від конфліктів між застосунками. Параметри в фразах (\(\.$query)) перетворюють фразу на параметризовану: Siri витягне частину висловлювання користувача і передасть її в намір.

Spotlight автоматично індексує всі AppShortcut-и, які ви оголосили. У iOS 17+ з'явилася окрема секція "Дії з застосунку" в результатах пошуку Spotlight, і ваші ярлики потрапляють туди без додаткової роботи. Для індексації окремих AppEntity-екземплярів (а не просто ярликів) скористайтеся IndexedEntity, що дозволяє Spotlight знаходити доменні об'єкти за вмістом.

Apple Intelligence та AssistantSchemas у iOS 26

Найбільша зміна iOS 26 для App Intents це AssistantSchemas. Це набір протоколів, які заявляють: "мій намір реалізує стандартну дію такого-то типу". Apple Intelligence знає семантику цих стандартних дій і може викликати ваш намір, коли користувач щось питає, навіть якщо фразування не збігається з оголошеними AppShortcut-фразами.

import AppIntents

@AssistantIntent(schema: .notes.createNote)
struct CreateNoteAssistantIntent: AppIntent {
    static let title: LocalizedStringResource = "Створити нотатку"

    @Parameter(title: "Назва")
    var title: String

    @Parameter(title: "Зміст", default: "")
    var content: String

    @MainActor
    func perform() async throws -> some IntentResult & ReturnsValue<NoteEntity> {
        let note = try await NoteStore.shared.create(title: title, body: content)
        return .result(value: note)
    }
}

Макрос @AssistantIntent(schema:) розгортається в conformance до конкретного протоколу схеми (тут це NotesCreateNoteIntent), який вимагає певний набір параметрів зі стандартизованими іменами. Apple Intelligence знає, що дія .notes.createNote приймає title та content, і коли користувач каже "запиши в нотатки список покупок на завтра", модель сама заповнить параметри й викличе намір, навіть якщо ви ніколи не оголошували відповідну фразу.

Доступні схеми покривають типові домени: notes, mail, messages, browser, files, system, photos. Якщо ваш застосунок не вкладається в жоден існуючий домен, ви все ще можете експонувати дії через звичайний AppIntent: модель буде використовувати їх через інтерфейс Shortcuts, але без семантичного розуміння.

Тестування та налагодження App Intents

App Intents це звичайні Swift-структури з async-методом perform(), тому їх тривіально тестувати без UI-фреймворка. Я зазвичай пишу тести в Swift Testing, бо @Test добре поєднується з async-кодом.

import Testing
@testable import NotesApp

@Suite("CreateNoteIntent")
struct CreateNoteIntentTests {
    @Test("створює порожню нотатку та повертає діалог")
    @MainActor
    func createsEmptyNote() async throws {
        let store = NoteStore.makeInMemory()
        NoteStore.shared = store

        let intent = CreateNoteIntent()
        let result = try await intent.perform()

        #expect(store.allNotes.count == 1)
        #expect(result.dialog?.value == "Нову нотатку створено.")
    }
}

Для налагодження живих викликів запустіть застосунок з Xcode і відкрийте Shortcuts. Намір з'явиться в списку доступних дій. Якщо його там немає, перевірте три речі:

  1. Тип публічний і не сховано за @available, що ваш target не задовольняє.
  2. title це LocalizedStringResource, а не звичайний String (інакше Shortcuts не індексує).
  3. Ви запустили білд хоча б один раз на пристрої або симуляторі, бо реєстрація відбувається при першому запуску застосунку.

Для Siri-тестування використовуйте Settings → Developer → Show App Intents, що відкриває переглядач зареєстрованих намірів з можливістю тестового запуску. У логах шукайте підсистему com.apple.appintents, бо туди фреймворк пише деталі resolution та помилок. Документація офіційного AppIntents-фреймворку містить актуальний перелік усіх протоколів і атрибутів. Корисний бонус: на WWDC-сесії з App Intents інженери Apple проходять реальні сценарії інтеграції з Apple Intelligence крок за кроком.

Типові помилки та як їх уникнути

За роки роботи з App Intents я постійно бачу одні й ті ж пастки. Перша це намагання заінжектити залежності через ініціалізатор. App Intents створюється системою через рефлексію і вимагає init() без аргументів; усі залежності беріть через @Dependency або синглтон-сховище.

struct ExportNotesIntent: AppIntent {
    static let title: LocalizedStringResource = "Експортувати нотатки"

    @Dependency var exporter: NoteExporter

    func perform() async throws -> some IntentResult {
        try await exporter.exportAll()
        return .result()
    }
}

// У `application(_:didFinishLaunchingWithOptions:)`:
AppDependencyManager.shared.add { NoteExporter.live }

Друга пастка це забути про openAppWhenRun. Дія, що оновлює UI, але не виносить застосунок у форграунд, виглядатиме як "нічого не сталося": користувач почує діалог, але змін у застосунку не побачить, доки не відкриє його руками. Я наступив на цей сценарій під час свого першого релізу з App Intents, і відгуки в TestFlight були не найласкавіші.

Третя це порожній або занадто загальний parameterSummary. Якщо ваш намір приймає три параметри і ви залишили дефолтний summary, у редакторі Shortcuts він виглядає як стіна окремих рядків. Витратьте п'ять хвилин на формулювання людською мовою.

Четверта це ігнорування того, як EntityQuery поводиться при першому запуску. Якщо ваше сховище порожнє і suggestedEntities() повертає порожній масив, Shortcuts відобразить намір без можливості вибору параметра, що збиває з пантелику. Поверніть невеликий набір "семплів" або позначте параметр як requiresInput.

І нарешті п'ята: пам'ятайте, що perform() має ідемпотентну семантику в очах системи. Apple Intelligence може повторити виклик, якщо мережа впала всередині. Якщо ваш намір створює сутність, перевірте на дублікат за зовнішнім ідентифікатором перед вставкою, або поверніть існуючий запис, що задовольняє запит.

Часті запитання

Чи можна використовувати App Intents без Apple Intelligence?

Так. App Intents працює на iOS 16+ і повністю функціональний у Shortcuts, Spotlight, Siri та на Action Button без Apple Intelligence. Інтеграція з Apple Intelligence через AssistantSchemas це додатковий шар, що активується лише на iOS 26 і сумісних пристроях.

Чим App Intents відрізняється від SiriKit?

SiriKit обмежений фіксованим набором доменів (повідомлення, платежі, тренування) і потребує окремого Intents Extension зі згенерованим Objective-C кодом. App Intents це Swift-only фреймворк без обмежень доменів, без файлів .intentdefinition і без окремого таргета розширення. Apple позиціонує App Intents як рекомендований шлях для всіх нових застосунків.

Як зробити так, щоб мій App Shortcut з'явився в Spotlight?

Оголосіть тип, що відповідає AppShortcutsProvider, і додайте до нього список AppShortcut-екземплярів. Spotlight автоматично індексує всі оголошені ярлики після першого запуску застосунку, тож додаткові виклики donate або NSUserActivity не потрібні.

Чи потрібно мені оголошувати окремі наміри для iOS та macOS?

Ні. Той самий тип AppIntent працює на всіх платформах, де доступний App Intents (iOS 16+, macOS 13+, watchOS 9+, visionOS 1+). За потреби розмежуйте поведінку через #if os(...) усередині perform() або позначте окремі параметри атрибутом @available.

Як передати дані між кількома App Intents у Shortcuts?

Поверніть результат, що відповідає ReturnsValue<T>, де T це AppEntity або примітивний тип. Користувач (або наступний намір у тому ж ярлику) зможе посилатися на повернене значення через "Magic Variables" в редакторі Shortcuts. Для chaining дій без участі користувача поверніть OpensIntent, що автоматично запустить наступний намір з переданим значенням.

Lukas Müller
Про Автора Lukas Müller

iOS developer and Swift author since the Objective-C days. Spends his evenings on side projects and his mornings on SwiftUI internals.