Capturing consistent color images
At a glance
| Item | Summary |
|---|---|
| Purpose | Capture Constant Color photos, optional fallback frames, confidence maps, and normal photos, then show/save the results. |
| App architecture | SwiftUI view owns an observable DataModel; the model owns a concrete Camera, which owns the capture session, outputs, delegates, and result state. |
| Main patterns | MVVM-style preview bridge, async stream, AVFoundation delegates, queue-confined session object. |
| Project style | Flat SwiftUI target with small view files, one view model, one large camera service, and an authorization helper. |
Project structure
ConstantColorCam/
├── ConstantColorCamApp.swift
├── ContentView.swift
├── CameraView.swift
├── DataModel.swift
├── Camera.swift
├── ViewFinderView.swift
├── PhotosTabView.swift
├── PhotoLibrary.swift
└── Assets.xcassets/
Structure observations
- View composition is split into focused SwiftUI files; capture and result mechanics are concentrated in
Camera.swift. DataModelhas one job: convert the camera’sCIImagepreview stream to SwiftUIImagestate.- There is no app-defined protocol between
DataModelandCamera; the relationship is concrete.
Overall architecture
flowchart LR
App["ConstantColorCamApp"] --> CameraView["CameraView"]
CameraView --> Model["DataModel"]
Model --> Camera["Camera"]
Camera --> Session["AVCaptureSession"]
Session --> Video["Video data output"]
Video --> Stream["AsyncStream<CIImage>"]
Stream --> Model
Model --> Finder["ViewFinderView"]
Session --> Photo["Photo output"]
Photo --> Camera
Camera --> Results["Captured image/confidence state"]
Results --> PhotosView["PhotosTabView"]
Camera --> Library["PhotoKit save"]
Reference code
ConstantColorCam/DataModel.swift:12 — preview bridge
var camera = Camera()
var viewfinderImage: Image?
func handleCameraPreviews() async {
let imageStream = camera.previewStream.map { $0.image }
for await image in imageStream {
Task { @MainActor in
viewfinderImage = image
}
}
}Interpretation
The observable model bridges the camera’s framework-oriented preview stream into SwiftUI image state. Camera setup, capture settings, photo classification, and saving all remain inside the concrete Camera object. Calling DataModel a view model is directly supported by its file comment; the broader architecture is MVVM-style rather than a fully abstracted MVVM stack.
Ownership and state
classDiagram
CameraView *-- DataModel : State creates
DataModel *-- Camera : creates
Camera *-- AVCaptureSession : creates
Camera *-- AVCapturePhotoOutput : creates during setup
Camera *-- AVCaptureVideoDataOutput : creates during setup
Camera *-- AsyncStream : creates
Camera --> PhotosTabView : supplies result values through CameraView
Ownership evidence
ConstantColorCam/CameraView.swift:11 and ConstantColorCam/DataModel.swift:10 — root model ownership
struct CameraView: View {
@State private var model = DataModel()
}
@Observable
final class DataModel {
var camera = Camera()
}| Owner | Object or state | Relationship | Mutation authority |
|---|---|---|---|
CameraView |
DataModel |
Creates and retains with @State |
SwiftUI retains model; model updates preview image. |
DataModel |
Camera and viewfinderImage |
Creates the camera and consumes its stream | DataModel writes preview state; camera’s app-internal properties remain technically mutable by module code. |
Camera |
Session, output objects, queue, preview continuation | Creates and retains capture resources | Camera, normally on its session/output queues. |
Camera |
Constant/fallback/normal photo and confidence results | Assigns in photo delegate callback | Setters are implicit internal, so the source does not enforce camera-only mutation. |
Class and protocol design
The sample defines no app-specific protocol for capture or preview. Camera directly conforms to AVFoundation delegate protocols in extensions; PhotoLibrary is a stateless authorization helper with a static method.
| Type | Responsibility | Depends on |
|---|---|---|
DataModel |
Convert streamed CIImage values to SwiftUI Image state |
Camera, Core Image, SwiftUI |
Camera |
Own capture graph, preview stream, capture settings, result processing, and saving | AVFoundation, Core Image, PhotoKit |
CameraView |
Own screen state, validate option combinations, send capture intent | DataModel/Camera |
ViewFinderView |
Display current preview image | SwiftUI binding |
PhotosTabView |
Present classified captured results | SwiftUI, UIKit images |
PhotoLibrary |
Check Photos authorization | PhotoKit |
Access control
| Symbol | Access | Verified effect | Likely rationale |
|---|---|---|---|
Camera.captureSession, inputs/outputs, queue, device lookup, helpers |
private |
Other files cannot directly reconfigure capture internals | Keeps session mechanics within Camera. |
CameraView.model and local UI toggles |
private |
Screen-owned state cannot be accessed by other files | Encapsulates transient presentation state. |
CIImage.image extension |
fileprivate |
Conversion helper is usable only within DataModel.swift |
Avoids making a one-consumer conversion a module-wide API. |
| Camera settings and captured-result properties | implicit internal, mutable |
Views in the module can read and assign them | Simple sample binding/coordination; weaker invariant enforcement than private(set). |
| App types | implicit internal |
Not exported outside the target | No public framework surface is intended. |
Reference code
ConstantColorCam/DataModel.swift:33 — same-file conversion helper
fileprivate extension CIImage {
var image: Image? {
let ciContext = CIContext()
guard let cgImage = ciContext.createCGImage(self, from: self.extent) else { return nil }
return Image(decorative: cgImage, scale: 1, orientation: .up)
}
}No public, open, or private(set) declaration appears in this sample’s Swift source.
Logic ownership and placement
| Logic | Owning type or file | Placement rationale |
|---|---|---|
| Preview conversion/state | DataModel |
The model bridges capture data to SwiftUI. |
| Option validation before capture | CameraView.takePhoto() |
Produces immediate presentation alerts before sending intent. |
| Session/device/output configuration | Camera |
Concrete owner of the AVFoundation graph. |
| Constant Color settings creation | Camera.takePhoto() |
Requires output capabilities and current camera options. |
| Result classification/confidence extraction | Camera photo-delegate extension | Receives AVCapturePhoto and writes captured-result state. |
| Result presentation | PhotosTabView |
Pure SwiftUI rendering of image values. |
Design patterns
| Pattern | Source evidence | Purpose or tradeoff |
|---|---|---|
| MVVM-style preview adapter | ConstantColorCam/DataModel.swift:4 and handleCameraPreviews() at line 21 |
Converts framework buffers into observable view state. |
| Async stream | ConstantColorCam/Camera.swift:143 and delegate yield at line 498 |
Decouples realtime preview production from SwiftUI consumption. |
| Delegate extensions | ConstantColorCam/Camera.swift:456, :464, and :498 |
Organizes readiness, photo, and video callbacks by AVFoundation protocol. |
| Session queue confinement | ConstantColorCam/Camera.swift:33 and setup/start calls |
Moves configuration and running changes off the main thread. |
| Concrete service object | ConstantColorCam/Camera.swift:15 |
Keeps sample flow visible, but combines capture, result state, and persistence. |
Main application flow
sequenceDiagram
actor User
participant View as CameraView
participant Camera
participant Output as AVCapturePhotoOutput
participant Results as PhotosTabView
User->>View: Tap shutter
View->>View: Validate option combination
View->>Camera: set options and takePhoto()
Camera->>Output: capturePhoto(settings, delegate)
Output-->>Camera: didFinishProcessingPhoto
Camera->>Camera: classify constant/fallback/normal result
Camera-->>Results: photosViewVisible and image state
Reference code
ConstantColorCam/Camera.swift:389 — capture settings handoff
func takePhoto() {
guard let photoOutput else { return }
let photoSettings = AVCapturePhotoSettings()
sessionQueue.async {
photoSettings.flashMode = self.flashEnabled ? .on : .off
photoSettings.isConstantColorEnabled = self.constantColorEnabled
photoSettings.isConstantColorFallbackPhotoDeliveryEnabled = self.fallBackPhotoDeliveryEnabled
photoOutput.capturePhoto(with: photoSettings, delegate: self)
}
}Naming conventions
- Types: concise responsibility nouns (
DataModel,Camera,PhotoLibrary) and SwiftUI role suffixes (CameraView,PhotosTabView). - Methods: command names (
takePhoto,switchCaptureDevice) and event/bridge names (handleCameraPreviews). - State: Boolean properties use
is...,...Enabled,...Supported,...Visible, or...Available. - Files: generally one primary type or closely related SwiftUI subviews per file.
- There are no app-defined protocol naming conventions to assess.
Architecture takeaways
- An
AsyncStreamis a compact bridge from capture callbacks to an observable SwiftUI model. DataModelremains narrow, butCameraintentionally combines session, result, and save logic for sample readability.fileprivatelimits the one-off Core Image conversion to its sole source file.- Unlike AVCam, captured-result and setting properties use mutable internal access; ownership is conventional rather than compiler-enforced.
- UI option validation lives in the screen, while AVFoundation capability/configuration checks stay in the camera object.
Source map
| Source file | Relevant symbols |
|---|---|
ConstantColorCam/ConstantColorCamApp.swift |
App entry |
ConstantColorCam/CameraView.swift |
Model owner, options, capture intent |
ConstantColorCam/DataModel.swift |
Observable preview view model and conversion helper |
ConstantColorCam/Camera.swift |
Capture graph, streams, delegates, results, save |
ConstantColorCam/ViewFinderView.swift |
Preview rendering |
ConstantColorCam/PhotosTabView.swift |
Result rendering |
ConstantColorCam/PhotoLibrary.swift |
Authorization helper |