Integrating your messaging app with Apple Intelligence
At a glance
| Item | Summary |
|---|---|
| Purpose | Expose messaging entities and send, draft, edit, read-status, and unsend operations to system experiences. |
| Architecture | One SwiftUI/SwiftData app with an actor-isolated data owner, a main-actor navigation owner, and thin App Intent adapters. |
| State strategy | ModelManager owns stored conversation state; NavigationManager owns foreground UI state; entity structs carry system-facing snapshots. |
| Boundary | App target only; there is no App Intents extension or widget target. |
Project structure
UnicornChat/
├── AppIntents/ # Schema intents, entities, queries, shortcuts
├── Managers/ # Data, navigation, notifications, generation
├── Model/ # SwiftData Contact/Conversation/Message
├── Extensions/ # Model fetching and entity projections
├── Views/ # Contact, conversation, and message features
└── UnicornChatApp.swift # Composition root
The folders separate framework-facing contracts from stored models and UI. Larger ModelManager responsibilities are split into source extensions without creating additional owners.
Overall architecture
flowchart LR
Root["UnicornChatApp"] --> Views["SwiftUI views"]
Root --> Model["ModelManager actor"]
Root --> Nav["NavigationManager"]
Root --> DI["AppDependencyManager"]
DI --> Intents["Message schema intents"]
DI --> Queries["Entity queries"]
Intents --> Model
Intents --> Nav
Model --> Store["SwiftData"]
Model --> Generator["ConversationManager / Foundation Models"]
Root --> Notifications["NotificationManager"]
Notifications --> Nav
Reference code
UnicornChat/UnicornChatApp.swift:22 — the app creates one object graph and registers the same managers for App Intents.
init() {
let model = ModelManager()
let navigation = NavigationManager()
AppDependencyManager.shared.add(dependency: navigation)
AppDependencyManager.shared.add(dependency: model)
NotificationManager.shared.configure(model: model, navigation: navigation)
self.navigation = navigation
self.model = model
}Views and background intent invocations converge on the same state owners. The intent structs translate system schemas; they do not create stores or navigation state.
Ownership and state
classDiagram
UnicornChatApp *-- ModelManager : creates and retains
UnicornChatApp *-- NavigationManager : creates and retains
ModelManager *-- ConversationManager : private owner
ModelManager *-- ModelContainer : model actor storage
NotificationManager o-- ModelManager : configured reference
NotificationManager o-- NavigationManager : configured reference
SendMessageIntent --> ModelManager : injected
DraftMessageIntent --> NavigationManager : injected
ConversationManager *-- Mutex : owns sessions
Ownership evidence
UnicornChat/Managers/ModelManager.swift:13 — the model actor privately owns the automatic-response service.
@ModelActor
actor ModelManager {
@MainActor var mainContext: ModelContext {
modelContainer.mainContext
}
private let conversationManager = ConversationManager()
}| Owner | Object or state | Relationship | Mutation authority |
|---|---|---|---|
UnicornChatApp |
ModelManager, NavigationManager |
Creates, retains, and injects | Wiring only |
ModelManager |
SwiftData models and ConversationManager |
Actor-isolated lifecycle owner | Conversation/contact/message persistence |
NavigationManager |
Selection, composition, and edit state | Main-actor observable owner | Foreground presentation state |
ConversationManager |
Per-recipient language-model sessions | Private mutex-protected owner | Session creation and response generation |
NotificationManager.shared |
Optional model/navigation references | Process singleton configured at launch | Notification scheduling and tap routing |
| App Intent structs | Injected managers | Borrow for one invocation | Parameter translation and orchestration only |
Class and protocol design
| Type | Design | Responsibility |
|---|---|---|
ModelManager |
@ModelActor actor |
Serialize SwiftData access and coordinate indexing, donations, and generated replies. |
ConversationManager |
final class: Sendable |
Use sendable snapshots and independent language-model sessions for concurrent recipients. |
NavigationManager |
@MainActor @Observable final class |
Hold view-only state and foreground routes. |
NotificationManager |
final class: NSObject, delegate extension |
Bridge UNUserNotificationCenterDelegate callbacks into model lookup and navigation. |
MessageEntity |
IndexedEntity, Transferable extension |
Adapt stored messages to Spotlight, App Intents, and transfer contracts. |
| Intent structs | AppIntent and message schema conformances |
Map system parameters to manager calls through @Dependency. |
Protocol-oriented design is concentrated at system boundaries (AppIntent, EntityQuery, IndexedEntity, Transferable, and UNUserNotificationCenterDelegate) rather than an app-defined service-protocol layer.
Access control
| Symbol | Access | Verified effect | Likely rationale |
|---|---|---|---|
ModelManager.conversationManager |
private let |
Only the persistence actor starts generated responses. | Preserve one orchestration boundary. |
ConversationManager.model, sessions, helpers |
private |
Callers can request responses but cannot manipulate sessions or prompts. | Protect concurrency and prompt invariants. |
NotificationManager initializer/references |
private |
Construction is limited to shared; retained dependencies change only through configure. |
Enforce singleton setup. |
| Managers, intents, entities | internal default | Cross-file code in the app target can collaborate; no external API is exported. | This is an app, not a reusable library. |
MessageTypingIndicator.body |
public within an internal type |
The member is explicit, but the containing view remains module-internal. | Likely conformance-style explicitness, not a package boundary. |
| Architecture types | no fileprivate or open |
No same-file-only collaboration layer or subclassable external API is defined. | The sample favors type-private invariants plus target-internal composition. |
Reference code
UnicornChat/Managers/NotificationManager.swift:18 — singleton creation and its mutable links are narrowly scoped.
nonisolated(unsafe) static let shared = NotificationManager()
private var model: ModelManager?
private var navigation: NavigationManager?
private override init() {}The nonisolated(unsafe) exception is limited to the singleton reference; configuration and delegate routing are explicitly main-actor operations.
Logic ownership and placement
| Logic | Owner | Placement rationale |
|---|---|---|
| Schema/container creation, CRUD, save, Spotlight, donation | ModelManager and its extensions |
One actor owns stored-state transitions. |
| Automatic replies | ConversationManager |
Foundation Models concurrency is isolated from SwiftData objects through snapshots. |
| Selection, compose, edit routes | NavigationManager |
UI state stays outside persistence. |
| Notification metadata and delegate callbacks | NotificationManager |
Keeps the Objective-C delegate bridge out of the data owner. |
| Entity lookup/projection | Entity queries and ModelManager+Entities |
System values resolve through stable identifiers. |
| Intent parameter validation | Individual intent perform() methods |
Unsupported schema combinations are rejected before domain mutation. |
Design patterns
| Pattern | Source evidence | Purpose or tradeoff |
|---|---|---|
| Composition root / dependency injection | UnicornChat/UnicornChatApp.swift:22 |
Shares one model and navigation graph across UI and intents. |
| Actor repository | UnicornChat/Managers/ModelManager.swift:13 |
Serializes SwiftData operations behind one owner. |
| Entity adapter | UnicornChat/AppIntents/MessageEntity.swift:90 |
Converts actor-bound models into system-facing value snapshots. |
| Router | UnicornChat/Managers/NavigationManager.swift:39 |
Centralizes foreground destinations. |
| Delegate adapter | UnicornChat/Managers/NotificationManager.swift:138 |
Translates notification callbacks into app operations. |
| Per-key session sharding | UnicornChat/Managers/ConversationManager.swift:59 |
Serializes dictionary access while allowing unrelated recipients to generate concurrently. |
Naming conventions
- Managers are named by owned capability:
ModelManager,ConversationManager,NavigationManager, andNotificationManager. - Stored domain nouns are un-suffixed (
Message,Conversation,Contact); system projections addEntity. - System commands use verb-noun
Intentnames such asSendMessageIntentandUnsendMessageIntent. - Methods distinguish commands (
sendMessage,openConversation) from lookups (entities(for:),fetchRecentMessages). - Files generally match their primary type; extensions group fetch, entity conversion, and error concerns around
ModelManager.
Architecture takeaways
- Register the same long-lived managers for views and App Intents instead of constructing a second state graph.
- Keep SwiftData mutation actor-isolated and cross that boundary with sendable snapshots for long-running generation work.
- Treat intents and entities as adapters around domain owners, not as owners themselves.
- Use weak/configured links for process services that route back into app state, and document narrow isolation exceptions.
Source map
| Source | Relevant symbols |
|---|---|
UnicornChat/UnicornChatApp.swift:12 |
App entry and dependency registration |
UnicornChat/Managers/ModelManager.swift:13 |
Actor repository and response orchestration |
UnicornChat/Managers/ConversationManager.swift:21 |
Sendable response generator and session sharding |
UnicornChat/Managers/NavigationManager.swift:12 |
Foreground state owner |
UnicornChat/Managers/NotificationManager.swift:14 |
Singleton notification/delegate bridge |
UnicornChat/AppIntents/MessageIntents.swift:30 |
Messaging schema intent adapters |
UnicornChat/AppIntents/MessageEntity.swift:15 |
Indexed entity and query |