From SwiftUI Features to App Intents: Designing System Entry Points
App Intents can expose application capabilities to Shortcuts, Siri, Spotlight, and other system surfaces. An intent should not call a SwiftUI button closure or copy a second version of business logic into an extension-shaped environment. A stronger architecture has both SwiftUI and the intent translate input into one domain command. The domain layer owns validation, authorization, idempotency, and persistence; the interface presents the result.
Begin with one narrow, complete action
A system action needs a clear verb, bounded parameters, and an explainable result: complete a task or log a glass of water. “Open the application and do anything” is too broad. “Turn the third row on the current screen green” depends on transient UI context. Choose an action that can recover from failure and does not require a complex interaction, then add composition later.
import AppIntents
struct CompleteTaskIntent: AppIntent {
static let title: LocalizedStringResource = "Complete Task"
@Parameter(title: "Task")
var task: TaskEntity
func perform() async throws -> some IntentResult & ProvidesDialog {
let command = CompleteTask(id: task.id)
let result = try await TaskCommands.shared.execute(command)
return .result(dialog: "Completed \(result.title)")
}
}
The intent is an adapter. TaskCommands must not import SwiftUI or require the current view hierarchy. An in-app button sends the same CompleteTask, so permission, synchronization, and error rules do not split into two implementations.
Keep the result honest. If the command only queued work, say it was queued rather than completed. If the operation requires connectivity, document the failure and retry behavior. A friendly dialog cannot substitute for a committed domain result.
AppEntity is a stable reference
The system must search and display selectable entities, but identifiers need to remain stable across launches. Queries should be bounded and honor account separation. Do not copy every sensitive database field into an AppEntity, and do not return an unlimited collection. Use localized display representation while retaining a durable key that reloads the current record at execution time.
Revalidate that the entity exists, belongs to the active account, and still permits the operation. A shortcut may run months after it was created, when cached text is stale. If a project was deleted or the user signed out, return an understandable error or request opening the application instead of crashing or acting on another account.
Entity queries are an API surface. Define search matching, ordering, pagination, and ambiguity. If two tasks have the same name, provide enough secondary information for a person to choose safely without disclosing private content on an exposed system surface.
Decide when the application must open
Only work that needs no additional confirmation, selection, or protected context should finish silently. Payments, broad deletion, and sharing-permission changes deserve confirmation and may need to continue in the application. A “hands-free” goal does not justify bypassing sign-in, biometric checks, or a product safety step.
An intent’s process and lifecycle differ from the foreground application. It may not have a navigation object, existing SwiftData context, or warm in-memory cache. Construct dependencies through a clear container, support cancellation, and bound network calls. When data is not synchronized, report that state instead of returning success and hoping an unowned background task finishes.
Put availability at the adapter boundary
If the application supports iOS 18 while a particular entry point uses an iOS 26 API, keep the domain command on the common deployment baseline and separate the newer declaration or parameter with availability. Do not let an older runtime load a path containing unavailable symbols. The system may render an intent through several interfaces, so its contract cannot rely on a particular button position, color, or dialog layout.
Localize titles, parameters, entity representations, and success and failure dialog. Every sentence must make sense without the application screen. Spoken invocation and VoiceOver benefit from short, distinct vocabulary; several intents with nearly identical names and different side effects are an avoidable hazard.
Test three contracts
Test the domain command directly for permission, idempotency, missing records, cancellation, and sync conflicts. Test entity queries against an isolated store for filtering, limits, stable identifiers, and account boundaries. Keep adapter tests focused on parameter mapping, dialog outcomes, and the branch that requests application continuation. Finally, run a small device suite through Shortcuts and the actual system surfaces.
Associate logs with an intent execution identity and domain command, but do not record private spoken input. Give retryable commands an idempotency key so a repeated system delivery or repeated tap cannot create two records. Observe latency and outcome separately; an invocation that returned quickly but failed later is not successful.
A good App Intent is not a remote control for a SwiftUI screen. It is a stable system interface to one domain action. Keep actions narrow, references durable, authorization fresh, risky effects confirmed, and foreground UI and system entry points on the same command path. New surfaces then increase reach without becoming a second backend that bypasses application rules.