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
AppEntityso 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:
EntityQuerymust 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) // fast path
}
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
// Used so we can call the intent from SwiftUI using .perform()
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
requestValueDialogto make Siri prompts natural. - Results: return entities when possible; system surfaces render them nicely.
- Isolation: mark with
@MainActoronly 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
AppIntentButtonto 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
AppEntitywithDisplayRepresentationandEntityQuery. - Implement a focused
AppIntentthat 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
- [ ] Entity has stable
id,typeDisplayRepresentation, and richdisplayRepresentation. - [ ] Queries are fast, cancellable, and return suggestions without opening the app.
- [ ] Intent reuses shared domain services; no duplicated business logic.
- [ ] Parameters are minimal and well phrased with
requestValueDialog. - [ ] Results return entities when possible for better system rendering.
- [ ] Strings are localized; tests cover queries and perform paths.