Powiem szczerze — gdyby ktoś zapytał mnie dziś, którego frameworka iOS-owego najmocniej żałuję, że nie ogarnąłem rok wcześniej, odpowiedź byłaby tylko jedna: App Intents. To dziś najważniejszy pomost między Twoją aplikacją a systemem — Siri, Spotlight, Shortcuts, widgety, Action Button i cała warstwa Apple Intelligence wchodzą do gry właśnie przez intenty.
W iOS 26 Apple dorzuciło coś, co naprawdę zmienia zasady gry: Interactive Snippets. Krótko mówiąc, możesz teraz zwrócić w pełni interaktywny widok SwiftUI prosto z intentu. Użytkownik rozmawia z Twoją apką w Siri, dotyka przycisków, widzi aktualizujący się interfejs — a aplikacja w ogóle się nie otwiera.
W tym przewodniku przejdziemy przez wszystko, co musisz wiedzieć o App Intents w 2026 roku. Od podstaw, przez parametry i AppShortcut, aż po nowy protokół SnippetIntent i jego cykl życia. Wszystkie przykłady są w Swift 6.2 i da się je wkleić do projektu w Xcode 26 (sprawdzałem).
Dlaczego App Intents są kluczowe w erze Apple Intelligence
Tradycyjnie aplikacje na iOS były zamkniętymi pudełkami. Chcesz coś zrobić? Otwierasz appkę. Apple Intelligence całkowicie odwraca ten model — Siri, Spotlight i nowe interfejsy AI same wywołują funkcje aplikacji, jeśli te są opisane jako App Intents.
I tu pojawia się brutalna prawda: aplikacja, która nie wystawia intentów, staje się dla systemu praktycznie niewidzialna. Asystent jej nie zaproponuje, Spotlight jej nie podpowie, użytkownik jej nie znajdzie. Boli, ale takie są realia.
App Intents zastępują dawne URL Schemes, NSUserActivity i ręczne integracje z Siri Shortcuts. To jeden, deklaratywny sposób, by powiedzieć systemowi: „oto co potrafi moja aplikacja, oto parametry, oto rezultat”. System sam zdecyduje, gdzie i kiedy uruchomić Twój kod — w odpowiedzi na zapytanie głosowe, sugestię w Spotlight, naciśnięcie Action Button albo automatyzację stworzoną przez użytkownika.
Co nowego w iOS 26
- Interactive Snippets — zwracaj widoki SwiftUI z przyciskami, które uruchamiają kolejne intenty.
SnippetIntent — nowy protokół z metodą reload() do animowanej aktualizacji widoku.
- Apple Intelligence Domains — predefiniowane schematy intentów (np.
BookReader, Photos), które Siri rozumie kontekstowo.
- Integracja z Foundation Models — wyniki
@Generable mogą stać się parametrami intentów.
- Visual Intelligence — App Intents są wywoływane przez kontekst wizualny (zdjęcie, screenshot).
Twój pierwszy App Intent — krok po kroku
Każdy intent to po prostu struktura zgodna z protokołem AppIntent. Wystarczą cztery elementy: tytuł, opis, parametry i metoda perform(). Tyle.
import AppIntents
import SwiftUI
struct LogWaterIntent: AppIntent {
static let title: LocalizedStringResource = "Zapisz wypitą wodę"
static let description = IntentDescription(
"Dodaje porcję wody do dziennego dziennika nawodnienia.",
categoryName: "Zdrowie"
)
@Parameter(
title: "Ilość (ml)",
default: 250,
controlStyle: .stepper,
inclusiveRange: (50, 1000)
)
var amount: Int
static var parameterSummary: some ParameterSummary {
Summary("Zapisz \(\.$amount) ml wody")
}
@MainActor
func perform() async throws -> some IntentResult & ProvidesDialog {
try await HydrationStore.shared.add(milliliters: amount)
let dialog = IntentDialog("Zapisałem \(amount) ml wody. Tak trzymać!")
return .result(dialog: dialog)
}
}
Kilka rzeczy wartych uwagi. parameterSummary mówi systemowi, jak skondensować intent do jednej linii w Shortcuts (bez tego użytkownik widzi tylko sucho nazwę intentu — wygląda kiepsko). IntentDialog to wypowiedź, którą Siri odczyta głosowo lub pokaże na ekranie po wykonaniu akcji. A controlStyle: .stepper dobiera optymalny widget UI w edytorze Shortcuts.
Wystawienie intentu jako fraza Siri — AppShortcut
Sam AppIntent trafia do Shortcuts, ale użytkownik wciąż musi go ręcznie znaleźć. Żeby Siri rozpoznawała komendę głosową od momentu instalacji, deklarujemy AppShortcut:
struct HydrationShortcuts: AppShortcutsProvider {
static var appShortcuts: [AppShortcut] {
AppShortcut(
intent: LogWaterIntent(),
phrases: [
"Zapisz wodę w \(.applicationName)",
"Dodaj \(\.$amount) ml wody w \(.applicationName)",
"Wypiłem szklankę w \(.applicationName)"
],
shortTitle: "Zapisz wodę",
systemImageName: "drop.fill"
)
}
static let shortcutTileColor: ShortcutTileColor = .blue
}
Każda fraza musi zawierać \(.applicationName) — to wymóg systemu, który zapobiega kolizjom między aplikacjami. Bez tego kompilator nie zaprotestuje (cisza, jak makiem zasiał), ale Siri nigdy nie rozpozna komendy. Spalony już raz jeden wieczór, zanim się zorientowałem.
AppEntity — modelowanie danych dla systemu
Jeśli intent zwraca lub przyjmuje obiekt z Twojej aplikacji (notatkę, projekt, kontakt — co tam masz), opisujesz go jako AppEntity. Dzięki temu Siri potrafi go nazwać, wyszukać i przekazać do innego intentu w łańcuchu.
struct Project: AppEntity {
static let typeDisplayRepresentation: TypeDisplayRepresentation = "Projekt"
static var defaultQuery = ProjectQuery()
let id: UUID
let name: String
let dueDate: Date
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(
title: "\(name)",
subtitle: "Termin: \(dueDate.formatted(date: .abbreviated, time: .omitted))"
)
}
}
struct ProjectQuery: EntityQuery {
func entities(for identifiers: [Project.ID]) async throws -> [Project] {
await ProjectStore.shared.projects(matching: identifiers)
}
func suggestedEntities() async throws -> [Project] {
await ProjectStore.shared.recent(limit: 5)
}
func entities(matching string: String) async throws -> [Project] {
await ProjectStore.shared.search(query: string)
}
}
Implementacja entities(matching:) jest tu kluczowa — pozwala Siri zrozumieć, kiedy użytkownik powie „pokaż projekt nawigacja” i dopasować to do Project o nazwie zawierającej słowo „nawigacja”. Bez tego dopasowywanie po nazwie nie zadziała.
Interactive Snippets — gwiazda iOS 26
No dobra, czas na bohatera tego wpisu.
Do iOS 26 intenty mogły zwracać tylko statyczne komunikaty albo prosty SnippetView, który rysował się raz i znikał. Teraz mamy SnippetIntent — protokół, który zwraca pełnoprawny widok SwiftUI, reaguje na akcje użytkownika i potrafi animować zmiany stanu. Wszystko bez otwierania aplikacji.
Architektura Interactive Snippet
Pomyśl o Interactive Snippet jak o maszynie stanów napędzanej kolejnymi intentami:
- Intent startowy (
AppIntent) zwraca ShowsSnippetIntent i wskazuje, który SnippetIntent ma się wyrenderować.
SnippetIntent w swojej metodzie perform() tworzy widok SwiftUI z aktualnymi danymi.
- Widok może zawierać tylko przyciski stworzone przez
Button(intent:label:) — każde naciśnięcie uruchamia kolejny AppIntent.
- Po wykonaniu intentu wywołujemy
SnippetIntent.reload(), system ponownie uruchamia perform() i animuje przejście do nowego widoku.
Pełny przykład — interaktywny licznik treningu
import AppIntents
import SwiftUI
// 1. Intent startowy — uruchamiany frazą Siri lub z Shortcuts
struct StartWorkoutIntent: AppIntent {
static let title: LocalizedStringResource = "Rozpocznij trening"
func perform() async throws -> some ShowsSnippetIntent {
let session = await WorkoutStore.shared.startSession()
return .result(snippetIntent: WorkoutSnippetIntent(sessionID: session.id))
}
}
// 2. SnippetIntent — wyświetla widok i go odświeża
struct WorkoutSnippetIntent: SnippetIntent {
static let title: LocalizedStringResource = "Snippet treningu"
@Parameter(title: "ID sesji") var sessionID: UUID
@Dependency var store: WorkoutStore
@MainActor
func perform() async throws -> some IntentResult & ShowsSnippetView {
let session = try await store.session(id: sessionID)
return .result(view: WorkoutSnippetView(session: session))
}
}
// 3. Widok SwiftUI z interaktywnymi przyciskami
struct WorkoutSnippetView: View {
let session: WorkoutSession
var body: some View {
VStack(spacing: 16) {
Text(session.exercise)
.font(.headline)
Text("\(session.reps) powtórzeń")
.font(.system(size: 48, weight: .bold, design: .rounded))
.contentTransition(.numericText(value: Double(session.reps)))
HStack(spacing: 24) {
Button(intent: ChangeRepsIntent(sessionID: session.id, delta: -1)) {
Label("Mniej", systemImage: "minus.circle.fill")
.labelStyle(.iconOnly)
.font(.largeTitle)
}
Button(intent: ChangeRepsIntent(sessionID: session.id, delta: 1)) {
Label("Więcej", systemImage: "plus.circle.fill")
.labelStyle(.iconOnly)
.font(.largeTitle)
}
}
Button(intent: FinishWorkoutIntent(sessionID: session.id)) {
Text("Zakończ trening")
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
}
.padding()
}
}
// 4. Intent zmieniający stan i odświeżający snippet
struct ChangeRepsIntent: AppIntent {
static let title: LocalizedStringResource = "Zmień liczbę powtórzeń"
@Parameter(title: "ID sesji") var sessionID: UUID
@Parameter(title: "Delta") var delta: Int
@Dependency var store: WorkoutStore
func perform() async throws -> some IntentResult {
try await store.adjust(sessionID: sessionID, by: delta)
await WorkoutSnippetIntent.reload()
return .result()
}
}
Zwróć uwagę na contentTransition(.numericText(...)) — system animuje liczby między reload'ami. Drobiazg, ale wygląda naprawdę dobrze.
Rejestracja zależności w punkcie startowym aplikacji
@Dependency pozwala wstrzykiwać współdzielone obiekty (store, repozytoria, klienci API) zarówno do intentów wywoływanych z aplikacji, jak i z procesów systemowych typu Siri czy Spotlight. Konfigurujemy to raz, w pliku App:
@main
struct WorkoutApp: App {
init() {
AppDependencyManager.shared.add(dependency: WorkoutStore.shared)
AppDependencyManager.shared.add(dependency: HydrationStore.shared)
}
var body: some Scene {
WindowGroup { ContentView() }
}
}
Pułapki, których musisz unikać
Lista, którą chciałbym mieć rok temu pod ręką:
- Brak
@State w SnippetView. Lokalny stan zostanie zignorowany przez system. Cała logika musi przepływać przez intenty.
- Tylko
Button(intent:label:). Standardowy Button z closure-action nie jest interaktywny w snippet — naciśnięcie zostanie zignorowane.
perform() musi być idempotentne. System może je uruchomić wielokrotnie (np. przy zmianie trybu jasny/ciemny). Nie modyfikuj stanu w SnippetIntent.perform() — tylko odczytuj.
- 4 KB limit danych. Cała struktura parametrów intentu musi się w to zmieścić. Trzymaj w intencie identyfikatory, nie całe obiekty.
- Frazy bez
\(.applicationName). Build przejdzie, ale Siri nie rozpozna komendy. Sprawdzaj zawsze na fizycznym urządzeniu — symulator potrafi tu kłamać.
Integracja z Apple Intelligence i Foundation Models
App Intents są bramą, przez którą Apple Intelligence wywołuje funkcjonalności Twojej aplikacji. W iOS 26 możesz zadeklarować, że Twój intent należy do jednej z Apple Intelligence Domains — predefiniowanych kategorii, które Siri rozumie semantycznie.
import AppIntents
import AppIntents.Domains.Photos
struct CreatePhotoAlbumIntent: AssistantIntent {
static let title: LocalizedStringResource = "Utwórz album"
static let assistantSchema: AssistantSchema = .photos.createAlbum
@Parameter(title: "Nazwa albumu") var name: String
@Parameter(title: "Zdjęcia") var photos: [PhotoEntity]
func perform() async throws -> some IntentResult & ReturnsValue {
let album = try await PhotoLibrary.shared.createAlbum(name: name, photos: photos)
return .result(value: album)
}
}
Dzięki assistantSchema Siri wie, że ten intent realizuje konkretny use case z Apple Intelligence i automatycznie mapuje na niego polecenia takie jak „utwórz album z wakacji z lipca”. Bez tego Siri trafia tylko po dosłownej frazie — a użytkownicy nigdy nie mówią dosłownie tak, jak zaprojektowałeś.
Łączenie Foundation Models z App Intents
Wynik wygenerowany przez @Generable z Foundation Models możesz potraktować jako parametr intentu. Pozwala to budować przepływy w stylu „Siri, podsumuj raport i utwórz zadanie z najważniejszych punktów”:
@Generable
struct TaskSuggestion {
@Guide(description: "Zwięzły tytuł zadania, max 60 znaków")
var title: String
@Guide(description: "Priorytet: low, medium, high")
var priority: String
@Guide(description: "Termin w formacie ISO 8601")
var dueDate: String
}
struct CreateTaskFromAIIntent: AppIntent {
static let title: LocalizedStringResource = "Utwórz zadanie z AI"
@Parameter(title: "Kontekst") var context: String
@Dependency var taskStore: TaskStore
func perform() async throws -> some IntentResult & ProvidesDialog {
let session = LanguageModelSession()
let response = try await session.respond(
to: "Na podstawie kontekstu zaproponuj zadanie: \(context)",
generating: TaskSuggestion.self
)
try await taskStore.create(from: response.content)
return .result(dialog: "Dodałem zadanie: \(response.content.title)")
}
}
Spotlight, Action Button i widgety
Najlepsze w tym wszystkim? Ten sam AppIntent działa we wszystkich wejściach systemu bez modyfikacji:
- Spotlight — App Shortcuts pojawiają się jako sugestie w wyszukiwarce systemowej. Użytkownik widzi Twoje akcje przy szukaniu „log wody” bez otwierania aplikacji.
- Action Button (iPhone 15 Pro+) — w iOS 26 można przypisać konkretny intent bezpośrednio do przycisku akcji.
- Interactive Widgets — używają tych samych
Button(intent:) co snippety. Kod współdzielisz między widget extension i aplikacją.
- Control Center Controls — od iOS 18 własne kontrolki w panelu sterowania uruchamiają App Intents.
Piszesz raz, działa wszędzie. Tak naprawdę.
Testowanie App Intents
Xcode 26 pozwala uruchamiać intenty bezpośrednio z kanwy Previews oraz z menu Debug → Test App Intents. Symulator wspiera debugowanie Siri — możesz mówić do mikrofonu Maca albo wpisywać frazy. Dla testów jednostkowych używaj nowego frameworka Swift Testing:
import Testing
import AppIntents
@testable import HydrationApp
@Test("LogWaterIntent zapisuje wodę w store")
@MainActor
func logWaterStoresAmount() async throws {
let store = HydrationStoreMock()
AppDependencyManager.shared.add(dependency: store)
var intent = LogWaterIntent()
intent.amount = 300
_ = try await intent.perform()
#expect(store.totalToday == 300)
}
Najczęściej zadawane pytania (FAQ)
Czym App Intents różnią się od dawnych Siri Intents (Intents Definition File)?
App Intents są w pełni deklaratywne w Swifcie — definiujesz strukturę, nie edytujesz pliku .intentdefinition. Wspierają znacznie więcej miejsc systemowych: Spotlight, widgety, Action Button, Apple Intelligence i Snippets. Dawne Siri Intents są nadal wspierane, ale Apple zaleca migrację — od iOS 26 wszystkie nowe API trafiają wyłącznie do App Intents.
Czy mogę używać @State i @Binding w widoku Interactive Snippet?
Nie w sposób, jakiego oczekujesz. Lokalny stan SwiftUI zostanie zignorowany — system odtwarza widok od zera przy każdym wywołaniu perform(). Aby przechowywać dane między interakcjami, trzymaj je w @Dependency (np. shared store) albo przekazuj jako parametry kolejnych intentów.
Jak debugować Interactive Snippet, jeśli nie aktualizuje się po naciśnięciu przycisku?
Sprawdź trzy rzeczy. Po pierwsze — czy używasz Button(intent:label:), bo inne inicjalizatory są nieinteraktywne. Po drugie — czy Twój intent w metodzie perform() wywołuje SnippetIntent.reload() dokładnie tego typu, który chcesz odświeżyć. Po trzecie — czy perform() w SnippetIntent faktycznie zwraca aktualne dane (częstym błędem jest cache'owanie wyniku poza dependency).
Czy App Intents działają na iPadzie, Macu i Apple Watch?
Tak. Ten sam kod działa na iOS 16+, iPadOS 16+, macOS 13+ i watchOS 10+. Interactive Snippets wymagają iOS 26, iPadOS 26 i macOS 26. Apple Intelligence Domains są dostępne tylko na urządzeniach wspierających Apple Intelligence (iPhone 15 Pro i nowsze, M-series Mac).
Co z Swift Packages — czy mogę umieścić App Intents poza głównym targetem aplikacji?
Możesz, ale z zastrzeżeniami. AppIntent i AppEntity działają w Swift Package świetnie — zalecam je tam umieszczać dla współdzielenia z widget extension. Natomiast AppShortcutsProvider historycznie działał wyłącznie w bundlu aplikacji. Apple poprawia to w iOS 26, ale w produkcji nadal najlepiej trzymać definicje shortcuts w głównym targecie i upewnić się, że frazy Siri zostały zarejestrowane po pierwszym uruchomieniu.
Podsumowanie
App Intents w iOS 26 to nie kolejny opcjonalny framework. To fundamentalny kontrakt między Twoją aplikacją a systemem. Interactive Snippets dają Ci możliwość zaprojektowania pełnoprawnych przepływów UX, które żyją poza ekranem startowym Twojej apki — w Siri, Shortcuts, Spotlight i Apple Intelligence.
Mój pomysł na start? Zacznij od jednego, dobrze opisanego AppIntent, dodaj do niego AppShortcut, a dopiero potem rozważ, które przepływy zyskają na zaprojektowaniu jako interaktywny snippet. Próba zrobienia wszystkiego naraz to prosta droga do tego, że projekt utknie na trzy tygodnie (mówię z doświadczenia).
I jeszcze jedno — jeśli pracujesz nad aplikacją w 2026 roku i nie masz jeszcze ani jednego App Intentu, zacznij od dziś. Naprawdę. To jedna z najbardziej dźwigniowych inwestycji, jakie możesz zrobić w widoczność swojej aplikacji w ekosystemie Apple.