| name | app-intent-driven-development |
| description | WHEN designing features App Intent-first so Siri/Shortcuts, widgets, and SwiftUI share logic; NOT UI-first; reuse intents across app. |
App Intent–First Driven Development
Design features as App Intents first, then reuse those intents across Shortcuts, widgets, and SwiftUI views so automation and UI stay in lockstep.
Core Ideas
- Entities first: model the data users act on (events, categories, records) as
AppEntity so intents, widgets, and the app share one source of truth.
- Intent-first feature: build the App Intent + entity query before UI; SwiftUI screens call those intents instead of duplicating service code.
- Single action, single intent: keep intents focused; avoid mega-intents that are hard to compose in Shortcuts.
- Predictable UI: supply
DisplayRepresentation, typeDisplayRepresentation, and icons so Siri/Shortcuts can render rich cards without opening the app.
- Fast queries:
EntityQuery must be quick and cancellable; avoid blocking the main actor.
- Reuse business logic: intents call the same services your views use; do not fork logic inside the intent.
Minimal Entity Blueprint
import AppIntents
struct TaskEntity: AppEntity, Identifiable {
static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "Task")
static let defaultQuery = TaskQuery()
let id: UUID
let title: String
let isComplete: Bool
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(
title: title,
subtitle: isComplete ? "Completed" : "Open",
image: .init(systemName: isComplete ? "checkmark.circle.fill" : "circle")
)
}
}
struct TaskQuery: EntityQuery {
func entities(for identifiers: [UUID]) async throws -> [TaskEntity] {
try await TaskStore.shared.fetch(ids: identifiers)
}
func suggestedEntities() async throws -> [TaskEntity] {
try await TaskStore.shared.fetchRecent()
}
}
Key points: stable identifier, meaningful representation, and fast queries that avoid launching heavy app flows.
Intent Pattern
import AppIntents
struct CompleteTaskIntent: AppIntent {
static let title: LocalizedStringResource = "Complete Task"
static let description = IntentDescription("Marks a task as done and returns the updated item.")
@Parameter(title: "Task", requestValueDialog: "Which task should I complete?")
var task: TaskEntity
init(task: TaskEntity) { self.task = task }
@MainActor
func perform() async throws -> some IntentResult & ReturnsValue<TaskEntity> {
let updated = try await TaskStore.shared.complete(task.id)
return .result(value: updated)
}
static var parameterSummary: some ParameterSummary {
Summary("Complete \(\.$task)")
}
}
- Parameters: keep them few; provide
requestValueDialog to make Siri prompts natural.
- Results: return entities when possible; system surfaces render them nicely.
- Isolation: mark with
@MainActor only if you must touch UI-bound objects; otherwise keep work off the main actor.
Reusing Intents in SwiftUI
- Prefer calling intents from UI so automation and in-app flows share one path.
- Use
AppIntentButton to invoke intents directly from views.
- Translate entity selections into view state so widgets/Shortcuts and in-app pickers present the same objects.
import AppIntents
import SwiftUI
struct EventRow: View {
let event: EventEntity
var body: some View {
HStack {
Text(event.name)
Spacer()
AppIntentButton(intent: UndoLastEventOccuranceIntent(event: event)) {
Label("Undo", systemImage: "arrow.uturn.backward")
}
}
}
}
- For more control, invoke intents imperatively with
perform (e.g., to show progress or handle errors):
import AppIntents
import SwiftUI
struct EventRow: View {
@Environment(\.intentExecutor) private var executor
@State private var isWorking = false
@State private var error: Error?
let event: EventEntity
var body: some View {
HStack {
Text(event.name)
Spacer()
Button {
Task {
isWorking = true
defer { isWorking = false }
do {
try await executor.perform(UndoLastEventOccuranceIntent(event: event))
} catch {
self.error = error
}
}
} label: {
if isWorking {
ProgressView()
} else {
Label("Undo", systemImage: "arrow.uturn.backward")
}
}
}
.alert("Undo failed", isPresented: .init(
get: { error != nil },
set: { if !$0 { error = nil } }
)) {
Button("OK", role: .cancel) { error = nil }
} message: {
Text(error?.localizedDescription ?? "Unknown error")
}
}
}
- Keep the intent signature identical between Shortcuts and SwiftUI usage.
- Avoid reimplementing service calls in views; route through the intent to keep analytics, validation, and side effects consistent.
Development Flow
- Model the domain type as
AppEntity with DisplayRepresentation and EntityQuery.
- Implement a focused
AppIntent that calls shared services; avoid duplicate data access layers inside the intent.
- Add previews in Shortcuts or the App Intents preview panel; ensure suggested entities show immediately.
- Expose the same entity in widgets/Live Activities to keep automation and UI consistent.
- Localize strings early (
LocalizedStringResource) to keep Siri responses natural in all supported languages.
Quick Checklist