Adopting App Intents to support system experiences
At a glance
| Item | Summary |
|---|---|
| Purpose | Demonstrate App Intents across navigation, Spotlight, visual search, snippets, transfer, shortcuts, and an interactive widget. |
| Architecture | Host app and widget consume a three-layer Swift package: Snippets → Intents → Core. |
| State strategy | Each process creates ModelData; an app-group SharedDataStore persists the small mutable subset shared across app and widget. |
| Boundary | App target, widget extension, and three library products are explicit; process memory is not shared. |
Project structure
AppIntentsTravelTracking/
├── AppIntentsTravelTracking/ # host app and views
├── TravelGalleryWidget/ # widget extension
└── TravelTrackingShared/
└── Sources/
├── TravelTrackingCore/
├── TravelTrackingIntents/
└── TravelTrackingSnippets/
Package dependencies point inward: views/targets may depend on shared libraries, while Core never imports the app target.
Overall architecture
flowchart LR
App["Host app"] --> Snippets["TravelTrackingSnippets"]
App --> Intents["TravelTrackingIntents"]
App --> Core["TravelTrackingCore"]
Widget["Widget extension"] --> Intents
Widget --> Core
Snippets --> Intents
Intents --> Core
Core --> Store["SharedDataStore / app group"]
App --> DI["AppDependencyManager"]
Widget --> DI2["Process-local dependency registry"]
Reference code
AppIntentsTravelTracking/TravelTrackingShared/Package.swift:21 — the package manifest enforces the layers (abridged).
products: [
.library(name: "TravelTrackingCore", targets: ["TravelTrackingCore"]),
.library(name: "TravelTrackingIntents", targets: ["TravelTrackingIntents"]),
.library(name: "TravelTrackingSnippets", targets: ["TravelTrackingSnippets"])
],
targets: [
.target(name: "TravelTrackingCore"),
.target(name: "TravelTrackingIntents", dependencies: ["TravelTrackingCore"]),
.target(name: "TravelTrackingSnippets", dependencies: ["TravelTrackingIntents"])
]Ownership and state
classDiagram
AppIntentsTravelTrackingApp *-- ModelData : State
AppIntentsTravelTrackingApp *-- Navigator : State
TravelGalleryWidgetBundle *-- ModelData : widget process
ModelData o-- SharedDataStore : singleton per process
SharedDataStore *-- UserDefaults : app-group suite
SharedDataStore *-- Mutex
AppIntent --> ModelData : injected
WidgetProvider --> SharedDataStore : reads index
Ownership evidence
AppIntentsTravelTracking/TravelTrackingShared/Sources/TravelTrackingCore/Persistence/SharedDataStore.swift:50 — the process-local singleton owns synchronized access to shared defaults.
public final class SharedDataStore: Sendable {
public static let shared = SharedDataStore()
public static let suiteName =
"group.com.example.apple-samplecode.AppIntentsTravelTracking"
nonisolated(unsafe) private let defaults: UserDefaults
private let lock = Mutex<Void>(())
}ModelData is the in-memory mutation owner. The store shares encoded values, not object identity, across the app and widget processes.
Class and protocol design
Core owns models/persistence; Intents owns public entities, queries, intents, and Navigator; Snippets owns interactive intent flows and their views. Types adopt specialized framework contracts such as IndexedEntity, Transferable, AppIntentTimelineProvider, and UndoableIntent. LandmarkEntityQuery combines EntityQuery, EntityStringQuery, EnumerableEntityQuery, and IndexedEntityQuery at AppIntentsTravelTracking/TravelTrackingShared/Sources/TravelTrackingIntents/Entities/LandmarkEntity.swift:130.
Access control
| Symbol | Access | Rationale |
|---|---|---|
| Package-facing models/entities/intents | public |
App and widget targets consume them across module boundaries. |
SharedDataStore.defaults and lock |
private |
Preserve typed and atomic access paths. |
nonisolated(unsafe) private defaults |
scoped concurrency escape | UserDefaults is documented thread-safe; the exception is limited to one field. |
| Widget provider helpers | private |
Timeline construction details remain inside the widget (AppIntentsTravelTracking/TravelGalleryWidget/TravelGalleryWidget.swift:37). |
| File logger | private |
Logging implementation does not widen the package API (AppIntentsTravelTracking/TravelTrackingShared/Sources/TravelTrackingCore/Persistence/ModelData.swift:13). |
The prevalence of public is evidence of true library boundaries, unlike target-only samples.
Logic ownership and placement
| Logic | Owner | Placement reason |
|---|---|---|
| Domain models and persisted shared state | Core | Used by app, intents, snippets, and widget. |
| Entity/query/intent mapping | Intents | System contracts depend on Core without UI imports. |
| Multi-step snippet UI | Snippets | Snippet rendering depends on intent types, not host views. |
| App navigation/rendering | Host app | Process-specific scene behavior stays outside libraries. |
| Widget timelines/actions | Widget target | WidgetKit lifecycle is an executable-target concern (AppIntentsTravelTracking/TravelGalleryWidget/TravelGalleryWidget.swift:16). |
Design patterns
| Pattern | Evidence | Purpose |
|---|---|---|
| Layered package | Manifest dependency graph | Prevents host/widget details from leaking into Core. |
| Composition root / DI | App registers ModelData, Navigator, SearchEngine (AppIntentsTravelTracking/AppIntentsTravelTracking/AppIntentsTravelTracking.swift:21) |
Supports intent/query construction. |
| Shared-store repository | Typed DefaultsKey + app-group suite |
Coordinates a bounded cross-process state subset. |
| Entity adapter/query | LandmarkEntity and query |
Publishes stable system-facing representations and Spotlight hooks. |
| Multi-target reuse | Widget links Core + Intents | Shares contracts while preserving separate lifecycle/state. |
Naming conventions
- Module names encode dependency purpose:
Core,Intents,Snippets. - System types use precise role suffixes (
Entity,Query,Intent,Provider,TimelineEntry). - Mutation APIs use domain verbs (
setLandmarkFavorite,setTags,applySavedState).
Architecture takeaways
- Use package targets when app and extension genuinely need shared compiled contracts.
- Share compact persisted values across processes, never assumptions about singleton identity.
- Keep system schema adapters outside Core domain models when they add framework dependencies.
- Register dependencies independently in every executable process (
AppIntentsTravelTracking/TravelGalleryWidget/TravelGalleryWidgetBundle.swift:13).
Source map
| Source | Role |
|---|---|
AppIntentsTravelTracking/AppIntentsTravelTracking/AppIntentsTravelTracking.swift:15 |
Host composition root |
AppIntentsTravelTracking/TravelTrackingShared/Sources/TravelTrackingCore/Persistence/ModelData.swift:15 |
Main-actor data owner |
AppIntentsTravelTracking/TravelTrackingShared/Sources/TravelTrackingCore/Persistence/SharedDataStore.swift:113 |
Shared mutations |
AppIntentsTravelTracking/TravelTrackingShared/Sources/TravelTrackingIntents/Intents/NavigateIntent.swift:11 |
Cross-platform navigation intent |
AppIntentsTravelTracking/TravelTrackingShared/Sources/TravelTrackingIntents/Entities/LandmarkEntity.swift:16 |
Indexed entity |