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) // 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 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
-
Entity has stable id , typeDisplayRepresentation , and rich displayRepresentation .
-
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.