Supporting HDR images in your app
At a glance
| Item | Summary |
|---|---|
| Purpose | Import, edit, render, preview, and export HDR still images while preserving extended dynamic range. |
| App architecture | MainView owns navigation and import/export state, creates an actor-isolated EditedAsset, and delegates Core Image rendering to Renderer; RenderView adapts the resulting pixel buffer to UIKit or AppKit. |
| Main patterns | Observable editing model, Renderer service, Platform view adapter |
| Project style | Ten Swift files; a cross-platform SwiftUI app with Core Image, Photos, document export, and thin UIKit/AppKit representables. |
Project structure
Source bundle/
├── HDRDemo23/
│ ├── HDRDemo23App.swift
│ ├── RenderView.swift
│ ├── AdjustView.swift
│ ├── ImageView.swift
│ ├── MainView.swift
│ ├── Renderer.swift
│ ├── Adjustment.swift
│ ├── Asset.swift
│ ├── EditedAsset.swift
│ └── FilmStrip.swift
├── Configuration/
│ └── SampleCode.xcconfig
└── HDRDemo23.xcodeproj/
└── .xcodesamplecode.plist
Structure observations
- The app entry creates
MainView; the image pipeline is not owned by an application delegate. EditedAssetcentralizes observable adjustment state and triggers rendering without overwriting the sourceAsset.- Conditional compilation is limited to the pixel-buffer platform adapter; editing and renderer types are shared.
Overall architecture
flowchart LR
APP["HDRDemo23App"] --> MAIN["MainView import, selection, export"]
MAIN --> SOURCE["Asset original image"]
MAIN --> EDIT["EditedAsset observable edit session"]
EDIT --> ADJ["Adjustment filters"]
ADJ --> RENDERER["Renderer and CIContext"]
RENDERER --> BUFFER["HDR CVPixelBuffer"]
BUFFER --> VIEW["RenderView / PixelBufferView"]
EDIT --> EXPORT["Photos or FileDocument export"]
Reference code
HDRDemo23/EditedAsset.swift:92 — adjustment-to-render pipeline
func applyAdjustments(_ adjustments: [Adjustment], showOriginal: Bool) async {
var image = inputImage
if !showOriginal {
for adjustment in adjustments {
image = adjustment.apply(to: image)
}
}
if let image,
let pixelBuffer = await renderer.render(image, destinationColorspace: inputImage?.colorSpace) {
setPixelBuffer(pixelBuffer)
}
}Interpretation
The view coordinates a user session, but the edit model owns the editable image state and render trigger. The renderer owns Core Image execution and HDR buffer construction. Presentation observes only the resulting buffer, keeping file/Photos workflows outside the drawing adapter.
Ownership and state
classDiagram
MainView *-- Renderer : creates
MainView *-- AssetArray : State
MainView *-- EditedAsset : State edit session
EditedAsset o-- Renderer : injected
EditedAsset o-- Asset : original
EditedAsset *-- AdjustmentArray : editable state
EditedAsset *-- CVPixelBuffer : latest render
RenderView --> EditedAsset : observes
Ownership evidence
HDRDemo23/MainView.swift:16 — feature-root state and renderer
var renderer = Renderer()
@State var selectedItems: [PhotosPickerItem] = []
@State var assets: [Asset] = []
@State var selectedAsset: Asset?
@State var editedAsset: EditedAsset?
@State var isEditing: Bool = false| Owner | Object or state | Relationship | Mutation authority |
|---|---|---|---|
MainView |
Renderer, imported assets, selection, edit session, presentation flags | Creates the renderer and stores SwiftUI state | It starts/cancels/saves edits and replaces selected assets. |
EditedAsset |
Original asset, adjustments, input image, latest pixel buffer | Retains injected collaborators and mutable edit state | Its setters schedule renders and its export methods materialize edits. |
Renderer |
Dispatch queue and CIContext |
Creates and retains rendering resources | Only the renderer allocates/renders/exports image output. |
PixelBufferView |
Platform layer contents | Receives a buffer value from RenderView |
The representable updates only the native layer. |
Composition denotes source-visible construction and retained value state. Aggregation denotes a stored or injected collaborator whose exclusive lifetime the source does not prove.
Class and protocol design
HDRDemo23/EditedAsset.swift:17 — observable main-actor edit model
@MainActor
@Observable
class EditedAsset: FileDocument {
nonisolated
static var readableContentTypes: [UTType] { [.image] }
let renderer: Renderer
let asset: Asset
var pixelBuffer: CVPixelBuffer? = nil
}| Type | Responsibility | Depends on or conforms to |
|---|---|---|
MainView |
Coordinates import, selection, edit, Photos save, and file export | View plus PhotosUI and SwiftUI document APIs |
EditedAsset |
Observable edit-session state and export representation | FileDocument, @Observable, @MainActor |
Renderer |
Renders filtered CIImage values into HDR buffers or encoded data |
Core Image, Core Video, ImageIO |
RenderView / PixelBufferView |
Bridges a rendered buffer into SwiftUI and a native layer | View; UIViewRepresentable or NSViewRepresentable |
Asset / Adjustment |
Represent original input and serializable filter choices | Value-oriented domain types |
FileDocument and the representable protocols are framework integration contracts. The sample defines no local service protocol; Renderer is injected concretely into each EditedAsset.
Access control
| Symbol | Access | Verified effect | Design reason |
|---|---|---|---|
Asset.loadThumbnail |
static private |
Callable only inside Asset’s lexical scope |
Thumbnail decoding is an implementation helper, not an app capability. |
EditedAsset and Renderer |
implicit internal |
Visible to the app module only | The sample is an application target and exports no reusable package API. |
@MainActor on EditedAsset |
actor isolation, not access control | Serializes observable edit state on the main actor | UI-facing mutation is protected from unsynchronized concurrency. |
nonisolated document requirements |
isolation exception, not public access |
Allows framework-required static/document witnesses outside actor isolation | Conformance requirements can be reached without treating them as an exported API. |
Reference code
HDRDemo23/EditedAsset.swift:21 — concurrency boundary on document requirements
nonisolated
static var readableContentTypes: [UTType] { [.image] }
nonisolated
static var writableContentTypes: [UTType] { [.heic, .png] }
let renderer: Renderer
let asset: Assetnonisolated changes actor isolation, not Swift visibility. Declarations without an access modifier remain internal; the only concrete private boundary highlighted here is the asset’s thumbnail loader.
Logic ownership and placement
| Logic | Owning type or file | Why it lives there |
|---|---|---|
| Import, selection, edit-mode transitions, save destination | MainView |
These decisions depend on view state and Photos/file presentation APIs. |
| Adjustment observation and render scheduling | EditedAsset |
The edit model owns the values whose mutation requires rerendering. |
| HDR buffer allocation, attachments, Core Image render/export | Renderer |
Rendering resources and color-space details stay out of views. |
| Native extended-range layer presentation | PixelBufferView |
Platform-specific layer setup is isolated behind a SwiftUI adapter. |
Design patterns
| Pattern | Source evidence | Purpose or tradeoff |
|---|---|---|
| Observable editing model | HDRDemo23/EditedAsset.swift:17 |
Binds adjustments and the latest render to SwiftUI while preserving the original asset. |
| Renderer service | HDRDemo23/Renderer.swift:14 |
Centralizes expensive Core Image resources and output-format details. |
| Platform view adapter | HDRDemo23/RenderView.swift:16 |
Keeps UIKit/AppKit layer code behind a shared SwiftUI surface. |
Naming conventions
- Domain nouns (
Asset,EditedAsset,Adjustment,Renderer) describe data ownership rather than screen position. - View types use a
Viewsuffix; platform bridgePixelBufferViewnames the value it presents. - Commands use workflow verbs (
toggleEdit,saveEdit,applyAdjustments,render,export). - Concurrency annotations are placed on the state-owning type or method, not encoded in names.
Architecture takeaways
- Keep import/export coordination in the feature view and image mutation in an observable edit-session model.
- Give color-space, pixel-buffer, and encoding details to one renderer service.
- Treat
@MainActorandnonisolatedas concurrency design, not visibility declarations.
Source map
| Source file | Architectural role |
|---|---|
HDRDemo23/HDRDemo23App.swift |
SwiftUI application entry |
HDRDemo23/MainView.swift |
Import/export and edit-session coordination |
HDRDemo23/EditedAsset.swift |
Observable editing state and document/Photos export |
HDRDemo23/Renderer.swift |
Core Image rendering and HDR output |
HDRDemo23/RenderView.swift |
SwiftUI-to-native pixel-buffer presentation |
HDRDemo23/Asset.swift / HDRDemo23/Adjustment.swift |
Source image and filter domain models |