App Intents v iOS 26: Kompletný sprievodca integráciou Apple Intelligence, Siri a interaktívnych snippetov

Naučte sa stavať App Intents pre iOS 26 — od prvej akcie cez App Shortcuts a parametre až po interaktívne snippety, Visual Intelligence a integráciu s Apple Intelligence. Praktický sprievodca s pracovnými ukážkami v Swifte 6.2.

App Intents iOS 26: Siri a Apple Intelligence

App Intents framework prešiel v iOS 26 najväčšou evolúciou od svojho uvedenia — a úprimne, bolo na čase. Pre vývojárov, ktorí chcú aby ich aplikácia žila aj mimo svojej ikony (v Spotlighte, Siri, Shortcuts, Action Buttone, Visual Intelligence a Apple Intelligence) sú App Intents pre rok 2026 jednoducho povinná výbava. Tento sprievodca vás prevedie celým ekosystémom: od prvého intentu cez App Shortcuts a entity až po novinky iOS 26 — interaktívne snippety, requestConfirmation(actionName:) a entity view annotations.

Príklady som písal v Swifte 6.2 a Xcode 26. Kde to dáva zmysel, upozorním aj na minimálne podporované staršie verzie OS.

Prečo App Intents v roku 2026 nie sú voliteľné

Apple Intelligence je centralizovaný personálny kontext systému. Keď používateľ povie Siri „pošli zápisník z dnešného mítingu Petrovi“, systém potrebuje vedieť, čo je vo vašej aplikácii „zápisník“, čo je „dnešný míting“ a ako sa „pošle“. A tieto informácie získava jediným spôsobom — cez App Intents. Je to oficiálna (a v podstate jediná) cesta, ktorou sa vaša aplikácia stáva čitateľnou pre Siri, Spotlight, Shortcuts a LLM-based akcie v Shortcuts (nové v iOS 26).

App Intents nie sú len Shortcuts API. Sú to verbs a nouns vašej aplikácie:

  • Verbs — akcie typu AppIntent (otvor poznámku, spusti tréning, pridaj položku na nákupný zoznam).
  • Nouns — entity typu AppEntity (poznámka, tréning, položka).
  • Queries — vyhľadávanie entít podľa názvu, identifikátora alebo sémantického dopytu (EntityQuery, EntityStringQuery, IntentValueQuery).

Keď máte tieto tri stavebné kamene pohromade, systém už dokáže sám vytvárať shortcuts, predikovať akcie, integrovať váš obsah do Visual Intelligence a sprístupniť entity Apple Intelligence cez on-screen entities. To je v podstate magické — a vy v kóde nemusíte pre to nič extra robiť.

Váš prvý App Intent

Začneme jednoduchým intentom, ktorý otvára konkrétnu poznámku. Predpokladajme, že máme typ Note a úložisko NoteStore.

import AppIntents
import SwiftUI

struct OpenNoteIntent: AppIntent {
    static let title: LocalizedStringResource = "Otvoriť poznámku"
    static let description = IntentDescription(
        "Otvorí konkrétnu poznámku v aplikácii.",
        categoryName: "Poznámky"
    )

    static var openAppWhenRun: Bool { true }

    @Parameter(title: "Poznámka")
    var note: NoteEntity

    @Dependency var router: AppRouter

    func perform() async throws -> some IntentResult & ProvidesDialog {
        await router.open(.note(id: note.id))
        return .result(dialog: "Otváram \(note.title).")
    }
}

Tri veci, ktoré stoja za pozornosť:

  1. openAppWhenRun hovorí systému, že akcia má vyústiť do otvorenia aplikácie. Pre intenty, čo bežia na pozadí (napr. „Pridaj 1 ks mlieka na nákupný zoznam“), ho nechajte na false.
  2. @Parameter definuje vstup. Systém sa naň pýta používateľa automaticky, ak ho nevie odvodiť z kontextu.
  3. @Dependency (od iOS 18, vylepšené v iOS 26) vám umožní injektovať singletony, routery a stores bez vlastného singleton patternu — registrujete ich v AppDependencyManager.

Registrácia závislostí

import AppIntents

@main
struct NotesApp: App {
    init() {
        AppDependencyManager.shared.add(dependency: AppRouter.shared)
        AppDependencyManager.shared.add(dependency: NoteStore.shared)
    }

    var body: some Scene {
        WindowGroup {
            RootView()
        }
    }
}

App Entities — slovník vašej aplikácie

Entita je doménový objekt, ktorý systém pozná a vie ho odovzdať ako parameter intentu, zobraziť v Shortcuts, použiť v on-screen Siri kontexte alebo vyhľadať cez Spotlight. Stručne: bez entít vaša aplikácia nemá pre systém žiadny slovník.

struct NoteEntity: AppEntity {
    static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "Poznámka")
    static let defaultQuery = NoteQuery()

    let id: UUID
    let title: String
    let preview: String
    let updatedAt: Date

    var displayRepresentation: DisplayRepresentation {
        DisplayRepresentation(
            title: "\(title)",
            subtitle: "\(preview)",
            image: .init(systemName: "note.text")
        )
    }
}

struct NoteQuery: EntityQuery {
    @Dependency var store: NoteStore

    func entities(for identifiers: [NoteEntity.ID]) async throws -> [NoteEntity] {
        await store.notes(matching: identifiers).map(NoteEntity.init)
    }

    func suggestedEntities() async throws -> [NoteEntity] {
        await store.recent(limit: 10).map(NoteEntity.init)
    }
}

extension NoteQuery: EntityStringQuery {
    func entities(matching string: String) async throws -> [NoteEntity] {
        await store.search(query: string).map(NoteEntity.init)
    }
}

Ak váš entitný typ konformuje aj k IndexedEntity, systém ho automaticky indexuje pre Spotlight a Visual Intelligence. Žiadny ďalší kód — vaše poznámky sa proste objavia vo vyhľadávaní. Z mojej skúsenosti je to jeden z tých najlacnejších featureov s najlepším pomerom hodnoty k práci.

App Shortcuts — vaše akcie viditeľné okamžite

App Shortcut je „kompilátorovo vyhlásená“ skratka — používateľ ju nemusí pridávať do Shortcuts aplikácie, je dostupná hneď po inštalácii. Zobrazujú sa v Spotlighte, ovládaní Action Buttonu, predikciách Siri a v iOS 26 aj v Visual Intelligence search panel.

struct NotesShortcuts: AppShortcutsProvider {
    static var appShortcuts: [AppShortcut] {
        AppShortcut(
            intent: OpenNoteIntent(),
            phrases: [
                "Otvor poznámku v \(.applicationName)",
                "Otvor \(\.$note) v \(.applicationName)"
            ],
            shortTitle: "Otvoriť poznámku",
            systemImageName: "note.text"
        )

        AppShortcut(
            intent: NewNoteIntent(),
            phrases: [
                "Nová poznámka v \(.applicationName)",
                "Pridaj poznámku do \(.applicationName)"
            ],
            shortTitle: "Nová poznámka",
            systemImageName: "square.and.pencil"
        )
    }

    static let shortcutTileColor: ShortcutTileColor = .grayBlue
}

Pravidlo č. 1: každá fráza musí obsahovať \(.applicationName). Bez toho Siri nevie deterministicky priradiť frázu k vašej aplikácii — a budete sa diviť, prečo akcia nikdy nebeží.

Pravidlo č. 2: v iOS 26 sa odporúča maximálne 10 App Shortcuts. Viac ich systém síce indexuje, ale Spotlight ich už nezobrazuje rovnocenne. Menej je v tomto prípade naozaj viac.

Novinka iOS 26: Interaktívne snippety

Toto je asi najväčšia funkčná zmena v App Intents tento rok — a osobne si myslím, že najpodceňovanejšia. Snippet je SwiftUI view, ktoré váš intent vracia ako svoj výsledok. Používateľ ho vidí v Siri, Shortcuts alebo Spotlighte bez toho, aby otváral vašu aplikáciu. A po novom (iOS 26) môžu byť tieto snippety interaktívne: tlačidlá, refresh, viackrokové formuláre.

Mentálny model snippetov

Snippet view nie je „obyčajný“ SwiftUI view. Pravidlá sú trochu zradné:

  • Tlačidlá musia používať iniciálnik Button(intent:label:). Bežný Button(action:) sa síce vyrenderuje, ale je neinteraktívny.
  • @State, TextField alebo Toggle s lokálnym binding nie sú interaktívne — text používateľa žiadate cez ďalší AppIntent s parametrami.
  • Stav žije medzi intentmi, nie vo view. Namiesto „state machine vo view“ myslite v termínoch chained intents a @Dependency.
  • Metóda perform() v SnippetIntent-e sa volá opakovane pri každom obnovení. Musí byť idempotentná, lacná a bez side-effectov.

Praktický príklad: interaktívny počítadlo kávy

Predstavme si tracker kávy (môj osobný favorit, lebo kávu pijem nezdravo veľa). Snippet zobrazí dnešný počet a používateľ ho môže priamo upraviť.

import AppIntents
import SwiftUI

@MainActor
final class CoffeeStore {
    static let shared = CoffeeStore()
    private(set) var todayCount: Int = 2

    func increment() { todayCount += 1 }
    func decrement() { todayCount = max(0, todayCount - 1) }
}

extension AppDependencyManager {
    static func bootstrap() {
        AppDependencyManager.shared.add(dependency: CoffeeStore.shared)
    }
}

Vstupný intent, ktorý sa vyvolá z fráz Siri alebo z App Shortcut:

struct ShowTodayCoffeeIntent: AppIntent {
    static let title: LocalizedStringResource = "Zobraz dnešnú kávu"
    static let description = IntentDescription("Otvorí interaktívny snippet s počtom šálok.")
    static var openAppWhenRun: Bool { false }

    func perform() async throws -> some ShowsSnippetIntent {
        .result(snippetIntent: CoffeeSnippetIntent())
    }
}

A teraz samotný snippet intent — toto je view-producujúci intent. Volá sa pri každom reload():

struct CoffeeSnippetIntent: SnippetIntent {
    static let title: LocalizedStringResource = "Káva — dnes"

    @Dependency var store: CoffeeStore

    @MainActor
    func perform() async throws -> some IntentResult & ShowsSnippetView {
        .result(view: CoffeeSnippetView(count: store.todayCount))
    }
}

struct CoffeeSnippetView: View {
    let count: Int

    var body: some View {
        VStack(spacing: 12) {
            Text("Dnes ste vypili")
                .font(.subheadline)
                .foregroundStyle(.secondary)

            Text("\(count) ☕️")
                .font(.system(size: 44, weight: .bold))
                .contentTransition(.numericText(value: Double(count)))

            HStack(spacing: 24) {
                Button(intent: DecrementCoffeeIntent()) {
                    Image(systemName: "minus.circle.fill")
                        .font(.title)
                }
                Button(intent: IncrementCoffeeIntent()) {
                    Image(systemName: "plus.circle.fill")
                        .font(.title)
                }
            }
        }
        .padding()
    }
}

A „mutujúce“ intenty s isDiscoverable = false — tie nemajú zmysel mimo snippetu, takže ich skrývame zo Shortcuts:

struct IncrementCoffeeIntent: AppIntent {
    static let title: LocalizedStringResource = "Pridať šálku"
    static var isDiscoverable: Bool { false }

    @Dependency var store: CoffeeStore

    @MainActor
    func perform() async throws -> some IntentResult {
        store.increment()
        await CoffeeSnippetIntent.reload()
        return .result()
    }
}

struct DecrementCoffeeIntent: AppIntent {
    static let title: LocalizedStringResource = "Odobrať šálku"
    static var isDiscoverable: Bool { false }

    @Dependency var store: CoffeeStore

    @MainActor
    func perform() async throws -> some IntentResult {
        store.decrement()
        await CoffeeSnippetIntent.reload()
        return .result()
    }
}

Statická metóda SnippetIntent.reload() je tu kľúčová. Bez nej by snippet ostal s pôvodným počtom — view sa nepretvára z lokálneho stavu. reload() povie systému, aby globálne refetchol entity-parametre a znovu zavolal perform() na pôvodnom snippet intente. Tento detail mi prvýkrát zabral asi hodinu zbytočného hľadania, takže verte mi: zapamätajte si to.

Viackrokové wizardy s requestConfirmation(actionName:snippetIntent:)

Druhá novinka iOS 26: requestConfirmation(actionName:snippetIntent:) dokáže vnútri bežiaceho intentu zobraziť ďalší snippet a počkať na potvrdenie. Skvelé pre objednávkové toky a všetko, kde chcete „dvojitý súhlas“.

struct OrderCoffeeIntent: AppIntent {
    static let title: LocalizedStringResource = "Objednať kávu"

    @Parameter(title: "Druh") var kind: CoffeeKindEntity
    @Parameter(title: "Veľkosť") var size: CoffeeSize

    func perform() async throws -> some IntentResult & ProvidesDialog {
        try await requestConfirmation(
            actionName: .order,
            snippetIntent: OrderSummarySnippetIntent(kind: kind, size: size)
        )

        let id = try await OrderService.shared.place(kind: kind, size: size)
        return .result(dialog: "Objednávka #\(id) je na ceste.")
    }
}

Snippet OrderSummarySnippetIntent môže obsahovať tlačidlá pre úpravu (cez ďalšie chained intenty) a nakoniec systém zobrazí natívne potvrdzovacie tlačidlo. Žiadne Apple Pay-style modaly už nie sú potrebné — a to je v zásade veľká úľava.

Visual Intelligence a IntentValueQuery

iOS 26 priniesol Visual Intelligence — používateľ podrží Camera Control alebo otvorí screenshot a systém ponúkne „pozri sa na to v <App>“. Aby sa vaša aplikácia v tomto paneli objavila, potrebujete IntentValueQuery:

struct PlantSpeciesQuery: IntentValueQuery {
    func values(for input: SemanticContentDescriptor) async throws -> [PlantSpeciesEntity] {
        guard let imageData = input.pixelBuffer.flatMap({ JPEGData(from: $0) }) else {
            return []
        }
        let candidates = try await PlantClassifier.shared.classify(imageData)
        return candidates.map(PlantSpeciesEntity.init)
    }
}

Tento query systém zavolá s pixel bufferom z fotoaparátu alebo screenshotu. Vaša aplikácia vráti najlepšie zhody ako entity, používateľ ich uvidí ako tile-y v search paneli a po klepnutí sa vyvolá váš OpenIntent.

extension PlantSpeciesEntity: OpenIntent {
    @MainActor
    func perform() async throws -> some IntentResult {
        await AppRouter.shared.open(.species(id: id))
        return .result()
    }
}

On-screen entities pre Apple Intelligence a ChatGPT

Druhá veľká integrácia. Keď používateľ aktivuje Siri (alebo „Ask ChatGPT“) na obrazovke, kde beží vaša aplikácia, systém potrebuje vedieť, na čo sa to vlastne pozerá. Vy mu to prezradíte cez focusedSceneValue a entity konformné s Transferable:

struct NoteDetailView: View {
    let note: Note

    var body: some View {
        ScrollView {
            // ...
        }
        .focusedSceneValue(\.currentNote, NoteEntity(note))
        .userActivity("com.swiftcrafted.notes.viewing", element: note) { activity in
            activity.appEntityIdentifier = note.id
            activity.title = note.title
        }
    }
}

extension NoteEntity: Transferable {
    static var transferRepresentation: some TransferRepresentation {
        DataRepresentation(exportedContentType: .pdf) { entity in
            try await PDFRenderer.render(noteId: entity.id)
        }
        ProxyRepresentation(exporting: \.title)
    }
}

Po tomto môžete v Siri pokojne povedať „pošli toto Petrovi“ a Siri vie, čo „toto“ je — názov, PDF, plain text. Apple Intelligence to aj zhrnie alebo preformuluje, ak treba.

Deferred properties — výkonová novinka iOS 26

Predtým systém pri zobrazovaní výsledku v Shortcuts musel vyhodnotiť všetky properties entity. Pri obrázkoch alebo súboroch to bolo poriadne drahé. iOS 26 teraz prináša @DeferredProperty:

struct PhotoEntity: AppEntity {
    let id: UUID
    let title: String

    @DeferredProperty
    var fullImage: IntentFile {
        get async throws {
            try await PhotoStore.shared.image(for: id)
        }
    }
}

Property sa vyhodnotí len vtedy, keď ju Shortcuts skutočne potrebuje. Ušetríte sieť, IO aj batériu — a používatelia s veľkými knižnicami fotiek vám budú ticho ďakovať.

Testovanie App Intents

So Swift Testing (Swift 6.2) sa intenty testujú elegantne. Trik je nahradiť závislosti pred spustením perform():

import Testing
@testable import NotesApp

@Suite("OpenNoteIntent")
struct OpenNoteIntentTests {
    @Test("Otvorí router so správnym route")
    @MainActor
    func opensRouter() async throws {
        let router = MockRouter()
        AppDependencyManager.shared.add(dependency: router)

        var intent = OpenNoteIntent()
        intent.note = NoteEntity(id: .init(), title: "Test", preview: "", updatedAt: .now)

        _ = try await intent.perform()

        #expect(router.lastRoute == .note(id: intent.note.id))
    }
}

Best practices a časté chyby

  • Lokalizujte všetko. LocalizedStringResource pre title, description, parameterSummary aj frázy App Shortcuts. Siri zlyhá ticho, ak fráza neexistuje v jazyku používateľa.
  • parameterSummary je takmer povinný pre dobrú UX v Shortcuts. Bez neho používateľ vidí len suchý zoznam parametrov.
  • Žiadne side-effecty v SnippetIntent.perform(). Volá sa opakovane.
  • Vyhnite sa openAppWhenRun = true, ak to nepotrebujete. Background intent + dialóg + snippet je takmer vždy lepší UX než vyrazenie používateľa z Lock Screenu.
  • Indexujte entity cez IndexedEntity. Spotlight, Visual Intelligence aj Apple Intelligence z toho profitujú zadarmo.
  • Limitujte počet App Shortcuts na ~10. Viac začne strácať efektivitu v Spotlighte.
  • Predávajte stav cez @Dependency, nie cez singletony. Ľahšie sa testuje a ľahšie nahrádza v previewoch.

Plán migrácie pre existujúcu aplikáciu

  1. Identifikujte 3–5 hlavných „verbs“ aplikácie. Začnite tými, ktoré používatelia robia najčastejšie.
  2. Vytvorte AppEntity pre 1–2 najdôležitejšie doménové objekty. Konformujte ich k IndexedEntity.
  3. Pridajte AppShortcutsProvider s frázami v lokalizačných súboroch.
  4. Pre kľúčovú akciu, ktorá dáva zmysel ako rýchly náhľad (dnešný stav, najnovšia notifikácia, krátky formulár), pridajte interaktívny snippet.
  5. Pre obrazovky s detailom doménového objektu nastavte focusedSceneValue a userActivity na on-screen entity.
  6. Pridajte IntentValueQuery, ak vaša aplikácia rozumie obrázkom (rastliny, jedlo, produkty, doklady).

Tento plán pokrýva zhruba 80 % integrácií, ktoré používateľ skutočne vidí, a stojí cca 1–2 sprinty práce. Z mojej skúsenosti je ROI pri prvých dvoch krokoch bezkonkurenčný.

Často kladené otázky

Aký je rozdiel medzi App Intents a SiriKit?

SiriKit pracoval s preddefinovanou množinou domén (správy, jazda, fitness…). App Intents je generický framework — môžete vyjadriť akúkoľvek akciu, akúkoľvek entitu, akýkoľvek dopyt. SiriKit je v iOS 26 pri novom vývoji ne-doporučený; všetka nová integrácia s Apple Intelligence ide cez App Intents.

Potrebujem App Intents, aby som podporoval Shortcuts aplikáciu?

Áno. App Intents je oficiálne API pre Shortcuts od iOS 16. Staré Intent Definition (.intentdefinition) súbory ešte fungujú, ale nemôžu byť použité v App Shortcuts ani v Apple Intelligence — odporúčam migráciu pri najbližšej príležitosti.

Sú interaktívne snippety dostupné aj v iOS 18?

Nie. Interaktívne snippety, SnippetIntent.reload() a requestConfirmation(actionName:snippetIntent:) sú novinky iOS 26. V iOS 17–18 môžete vrátiť statický snippet view ako ShowsSnippetView, ale interaktívne prvky tam jednoducho nepôjdu.

Ako odlíšim App Intent určený pre Apple Intelligence od bežného Shortcuts intentu?

A toto je dobrá správa — nepotrebujete to vôbec odlišovať. Ten istý AppIntent môže bežať z Shortcuts, Spotlightu, Action Buttonu, Siri aj Apple Intelligence. Dôležité je iba: čistý parameterSummary, kvalitný description, lokalizované frázy a entity konformné k IndexedEntity. Apple Intelligence si zvyšok zoradí samo.

Ako fungujú App Intents v App Clips alebo widgetoch?

Widgety (vrátane Live Activities) môžu spúšťať App Intents priamo cez Button(intent:) a Toggle(isOn:intent:) bez otvorenia hostiteľskej aplikácie. App Clips podporujú obmedzenú množinu intentov — typicky tie s openAppWhenRun = false. V oboch prípadoch sú intenty rovnaké, len perform() nesmie závisieť od plnej aplikačnej databázy.

Čo s lokalizáciou fráz pre Slovensko?

Slovenský jazyk pre Siri je v iOS 26 podporovaný cez Apple Intelligence (vyžaduje stiahnutie modelu). App Shortcuts frázy lokalizujte v AppShortcuts.xcstrings a používajte prirodzenú slovenskú konjugáciu — Siri matchuje ľubovoľné gramatické tvary, ak ich máte v stringoch. Aspoň 3 alternatívne frázy na každý intent sú dobrou hygienou.

Záver

App Intents v iOS 26 sú most medzi vašou aplikáciou a celým ekosystémom Apple Intelligence. Začnite jedným intentom a jednou entitou — odmena príde okamžite cez Spotlight a App Shortcuts. Postupne pridajte interaktívne snippety pre rýchlu interakciu, on-screen entities pre Siri a IntentValueQuery pre Visual Intelligence. Výsledkom je aplikácia, ktorá nežije len v ikonke na ploche, ale na každom mieste, kde používateľ niečo robí. A to je presne tá budúcnosť, do ktorej Apple smeruje — bolo by škoda na ňu nenastúpiť.

O Autorovi Tomasz Wojcik

Tomasz is a Krakow-based iOS engineer with 11 years of Swift experience. He spent four years at Revolut on the Wealth team, where he rewrote the trading charts in SwiftUI and shaved 40% off cold-start time by lazy-loading the analytics SDK. Before Revolut he was at Allegro, Poland's largest e-commerce platform, on the Seller Center iOS team. His specialty is iOS performance work: Instruments deep-dives, memory-graph debugging, and figuring out why your scroll view drops frames only on iPhone SE 2nd-gen. He has contributed patches to swift-syntax and writes a quarterly newsletter for iOS engineers that covers under-discussed APIs like BackgroundTasks and NSFileCoordinator. Tomasz holds the iOS App Development with Swift certification from Apple and occasionally runs paid workshops on Swift concurrency for in-house engineering teams in Europe.