Integrating your photo app with Apple Intelligence
At a glance
| Item |
Summary |
| Purpose |
Adopt Photos-domain entities and create, update, delete, open, search, and album-membership schemas. |
| Architecture |
One SwiftUI app wraps PhotoKit with an observable media-library facade, an actor image cache, and a navigation state owner. |
| State strategy |
MediaLibrary owns the current asset/album collections; Asset and Album wrap PhotoKit objects; entities are system-facing snapshots. |
| Boundary |
App target only; there is no App Intents extension, widget, or separate package. |
Project structure
PhotosDomainExample/
├── AppIntents/
│ ├── Album/ # Album entity, enum, and intents
│ ├── Asset/ # Asset entity, filters, and intents
│ └── ShortcutsProvider.swift
├── Managers/ # Media, image, location, navigation services
├── Model/ # PhotoKit-backed Album and Asset wrappers
├── Views/ # Album, asset, and favorites UI
└── PhotosDomainExample.swift # Composition root
Feature-oriented App Intent folders sit beside framework-neutral manager and model folders. The app keeps system schema declarations separate from PhotoKit wrappers.
Overall architecture
flowchart LR
Root["PhotosDomainExample"] --> Views["SwiftUI views"]
Root --> Library["MediaLibrary"]
Root --> Nav["NavigationManager"]
Root --> DI["AppDependencyManager"]
DI --> Intents["Photos schema intents"]
DI --> Queries["Entity queries"]
Intents --> Library
Intents --> Nav
Queries --> Library
Library --> Models["Album / Asset wrappers"]
Library --> PhotoKit["PHPhotoLibrary"]
Library --> Spotlight["Core Spotlight"]
Views --> Images["ImageManager actor"]
Reference code
PhotosDomainExample/PhotosDomainExample.swift:19 — the app gives views and intents the same library and navigation instances.
init() {
let navigationManager = NavigationManager()
let library = MediaLibrary()
library.load()
AppDependencyManager.shared.add(dependency: library)
AppDependencyManager.shared.add(dependency: navigationManager)
self.library = library
self.navigationManager = navigationManager
}
The same objects are also placed in the SwiftUI environment. This keeps system-triggered work and foreground UI synchronized without an extension-specific store.
Ownership and state
classDiagram
PhotosDomainExample *-- MediaLibrary : creates and retains
PhotosDomainExample *-- NavigationManager : creates and retains
MediaLibrary *-- Asset : collection snapshots
MediaLibrary *-- Album : collection snapshots
Asset o-- PHAsset : wraps
Album o-- PHAssetCollection : wraps
ImageManager *-- PHCachingImageManager : private cache
AlbumQuery --> MediaLibrary : injected
OpenAssetIntent --> NavigationManager : injected
Ownership evidence
PhotosDomainExample/Managers/MediaLibrary.swift:21 — collection mutation is restricted to the library facade.
private(set) var assets: [Asset] = []
private(set) var albums: [Album] = []
| Owner |
Object or state |
Relationship |
Mutation authority |
PhotosDomainExample |
MediaLibrary, NavigationManager |
Creates, retains, and injects |
Lifecycle and wiring |
MediaLibrary |
Asset/album arrays |
Observable owner with externally read-only collections |
Load, create, import, delete, and indexing coordination |
Asset / Album |
PhotoKit object plus mutable item snapshot |
Wrapper retains a framework object |
Item property and album-membership changes |
ImageManager.shared |
PHCachingImageManager, cached IDs/options |
Actor singleton and private owner |
Image cache and request serialization |
NavigationManager |
Navigation paths, selection, search term |
Observable foreground owner |
Open and search presentation state |
| Intent/query values |
Injected managers |
Borrow per invocation |
Resolve, validate, and delegate only |
Class and protocol design
| Type |
Design |
Responsibility |
MediaLibrary |
@Observable final class |
Facade over authorization, collection loading, create/delete operations, and Spotlight synchronization. |
Asset / Album |
@Observable final class, Identifiable, Hashable |
Give PhotoKit references stable app identity and mutation methods. |
ImageManager |
actor singleton |
Serialize access to the mutable PhotoKit caching manager. |
NavigationManager |
@Observable final class |
Route system-selected assets, albums, and searches into SwiftUI state. |
AlbumEntity / AssetEntity |
schema AppEntity, IndexedEntity |
Adapt app wrappers to system-visible values. |
| Intent structs |
AppIntent, OpenIntent, or DeleteIntent |
Bind Photos/system schemas to app operations. |
There is no app-defined repository protocol. Protocol-oriented design appears at framework seams (IndexedEntity, EntityQuery, OpenIntent, and DeleteIntent), while the concrete MediaLibrary is injected directly.
Access control
| Symbol |
Access |
Verified effect |
Likely rationale |
MediaLibrary.assets, albums |
private(set) |
Views, queries, and intents can read collections; only the library replaces or appends them. |
Preserve collection/index synchronization. |
| Fetch and authorization helpers |
private |
Callers use load rather than partially running the load pipeline. |
Keep permission and indexing behavior together. |
| PhotoKit helper functions |
nonisolated private static |
Helpers can run away from actor-isolated state but remain callable only inside MediaLibrary. |
Move pure PhotoKit work off the UI path without widening API. |
ImageManager cache/options |
private |
Consumers can request/cancel caching but cannot mutate the underlying manager. |
Protect cache bookkeeping. |
| App, managers, models, intents, entities |
internal default |
Types collaborate across app files but are not exported. |
No reusable framework API is needed. |
| Architecture types |
no fileprivate, public, or open |
There is no file-only collaboration surface, exported API, or subclassing contract. |
Type-private invariants and target-internal composition are sufficient. |
Reference code
PhotosDomainExample/Managers/MediaLibrary.swift:156 — off-main helpers stay both stateless and type-private.
// These run off the main actor so the library iteration and Photos
// change-blocks never block UI. They are pure helpers — they do not
// touch any actor-isolated state.
nonisolated private static func fetchAllPhotoAssets() async -> [PHAsset] {
// ...
}
Logic ownership and placement
| Logic |
Owner |
Placement rationale |
| Authorization, load, create/import/delete, Spotlight |
MediaLibrary |
Collection state and external side effects change together. |
| Favorite/hidden/title/album-membership mutations |
Asset and Album |
Item wrappers update PhotoKit and their observable snapshots in one method. |
| Image caching and async image requests |
ImageManager |
An actor protects mutable caching resources from concurrent callers. |
| Foreground routes and search |
NavigationManager |
Presentation state is independent of PhotoKit storage. |
| Schema conversion and lookup |
Entity files and nested query types |
Keeps system contracts adjacent to their entity representation. |
| Parameter validation and orchestration |
Intent perform() methods |
System optionality is resolved before delegating to managers/models. |
Design patterns
| Pattern |
Source evidence |
Purpose or tradeoff |
| Composition root / DI |
PhotosDomainExample/PhotosDomainExample.swift:19 |
Shares state owners across UI and App Intent execution. |
| Repository facade |
PhotosDomainExample/Managers/MediaLibrary.swift:26 |
Provides one entry point for collection loading and structural mutations. |
| Active record-like wrapper |
PhotosDomainExample/Model/Asset.swift:86 |
Keeps per-item PhotoKit mutation with the observable item snapshot. |
| Entity adapter |
PhotosDomainExample/Model/Album.swift:90 |
Projects framework-backed objects into schema values. |
| Actor service |
PhotosDomainExample/Managers/ImageManager.swift:11 |
Serializes mutable image-cache access. |
| Router |
PhotosDomainExample/Managers/NavigationManager.swift:28 |
Converts intent-driven opens/searches into SwiftUI navigation state. |
Naming conventions
- PhotoKit-backed domain wrappers use plain nouns (
Asset, Album); system values append Entity.
- Long-lived capabilities append
Manager: MediaLibrary is the deliberate domain-named exception and reads as a facade.
- Commands use verb-noun intent names (
CreateAssetsIntent, OpenAlbumIntent, DeleteAssetsIntent).
- Mutation methods begin with verbs (
create, delete, set, open); lookup methods use assets(for:) and albums(for:).
- App Intent folders are organized by schema noun, while view folders mirror UI features.
Architecture takeaways
- Share one observable framework facade between views and intents when both run in the app target.
- Use
private(set) for collection snapshots whose writes must stay synchronized with an external framework and index.
- Keep collection-level operations in the library facade and item-level mutations on the PhotoKit-backed wrappers.
- Put mutable image caching behind an actor and keep navigation in a separate observable owner.
- Prefer framework protocol conformances for system integration; introduce custom protocols only when a real replacement seam is needed.
Source map
| Source |
Relevant symbols |
PhotosDomainExample/PhotosDomainExample.swift:11 |
App entry and dependency registration |
PhotosDomainExample/Managers/MediaLibrary.swift:12 |
Photo library facade and collection owner |
PhotosDomainExample/Managers/ImageManager.swift:11 |
Actor image cache |
PhotosDomainExample/Managers/NavigationManager.swift:11 |
Foreground router |
PhotosDomainExample/Model/Asset.swift:14 |
PhotoKit-backed asset wrapper |
PhotosDomainExample/Model/Album.swift:11 |
PhotoKit-backed album wrapper |
PhotosDomainExample/AppIntents/Album/AlbumEntity.swift:10 |
Indexed album schema entity/query |
PhotosDomainExample/AppIntents/Album/AlbumIntents.swift:11 |
Album schema adapters |
PhotosDomainExample/AppIntents/Asset/AssetIntents.swift:16 |
Asset and system-search adapters |