AVCam: Building a camera app
At a glance
| Item | Summary |
|---|---|
| Purpose | Capture photos and movies, expose capture features to SwiftUI, and save media to Photos. |
| App architecture | SwiftUI MVVM-style app: CameraView consumes the Camera capability, CameraModel owns UI state, and actor services own capture and library work. |
| Main patterns | Protocol-oriented view model, actor isolation, composed output services, UIKit preview adapter, async streams/observation. |
| Project style | Feature folders for Capture, Model, Views, Support, plus main app, locked-camera capture extension, and Control Center extension targets. |
Project structure
AVCam/
├── AVCamApp.swift
├── CameraModel.swift
├── CameraView.swift
├── CaptureService.swift
├── Capture/
│ ├── DeviceLookup.swift
│ ├── PhotoCapture.swift
│ ├── MovieCapture.swift
│ └── SPCObserver.swift
├── Model/
│ ├── Camera.swift
│ ├── CameraState.swift
│ ├── DataTypes.swift
│ ├── Intent/AVCamCaptureIntent.swift
│ └── MediaLibrary.swift
├── Views/
│ ├── CameraPreview.swift
│ ├── Controls/
│ ├── Overlays/
│ └── Toolbars/
└── Support/
AVCamCaptureExtension/
AVCamControlCenterExtension/
Structure observations
- Capture mechanics are separated from observable screen state and from SwiftUI view composition.
PhotoCaptureandMovieCaptureisolate output-specific behavior underCapture/; shared app-facing contracts and value types live underModel/.- Preview bridging lives with views because it adapts an
AVCaptureVideoPreviewLayer-backedUIViewto SwiftUI. - The app, capture extension, and Control Center extension are separate targets but share model intent/state concepts.
Overall architecture
flowchart LR
App["AVCamApp"] --> View["CameraView<Camera>"]
App --> Model["CameraModel"]
View --> Model
Model --> Capture["CaptureService actor"]
Model --> Library["MediaLibrary actor"]
Capture --> Outputs["PhotoCapture + MovieCapture"]
Capture --> Session["AVCaptureSession"]
Library --> Photos["PhotoKit"]
Reference code
AVCam/AVCamApp.swift:13 — composition root
struct AVCamApp: App {
@State private var camera = CameraModel()
var body: some Scene {
WindowGroup {
CameraView(camera: camera)
.task {
await camera.start()
}
}
}
}Interpretation
AVCamApp is the composition root and long-lived owner of CameraModel. The model mediates all view intents and observable state; it delegates capture and photo-library work instead of owning framework details. Calling this MVVM is an architectural interpretation supported by the source comments and dependency direction, not a type named ViewModel.
Ownership and state
classDiagram
AVCamApp *-- CameraModel : State creates
CameraView --> CameraModel : observes and sends intents
CameraModel *-- CaptureService : creates
CameraModel *-- MediaLibrary : creates
CaptureService *-- AVCaptureSession : creates
CaptureService *-- PhotoCapture : creates
CaptureService *-- MovieCapture : creates
CaptureService *-- DefaultPreviewSource : exposes capability
Ownership evidence
AVCam/CameraModel.swift:63 — model dependencies
private let mediaLibrary = MediaLibrary()
private let captureService = CaptureService()
private var cameraState = CameraState()| Owner | Object or state | Relationship | Mutation authority |
|---|---|---|---|
AVCamApp |
CameraModel |
Creates and retains it with SwiftUI @State |
CameraModel mutates its observable state on MainActor. |
CameraModel |
UI-facing status, activity, readiness, thumbnail, error | Publishes read-only state to views | Setters are restricted to CameraModel. |
CameraModel |
CaptureService, MediaLibrary, CameraState |
Creates and retains each collaborator | Model sends intents; actors own their internal state. |
CaptureService |
Capture session, device input, photo/movie services | Creates, configures, and retains capture graph | CaptureService actor on its serial executor. |
PhotoCapture / MovieCapture |
Their AVCaptureOutput and delegate lifecycle |
Own output-specific capture work | Each service mutates its own internals. |
Class and protocol design
classDiagram
class Camera {
<<protocol>>
}
class OutputService {
<<protocol>>
}
class PreviewSource {
<<protocol>>
}
class PreviewTarget {
<<protocol>>
}
CameraModel ..|> Camera
PreviewCameraModel ..|> Camera
CameraView --> Camera
PhotoCapture ..|> OutputService
MovieCapture ..|> OutputService
DefaultPreviewSource ..|> PreviewSource
CameraPreview --> PreviewSource
CameraPreview ..> PreviewTarget
Reference code
AVCam/CameraView.swift:12 — protocol-oriented view dependency
@MainActor
struct CameraView<CameraModel: Camera>: PlatformView {
@State var camera: CameraModel
// ...
}| Type | Responsibility | Depends on |
|---|---|---|
Camera |
Define the complete view-facing camera capability | App data types and PreviewSource |
CameraModel |
Translate UI intent to services and service streams to observable state | CaptureService, MediaLibrary, CameraState |
CaptureService |
Configure and serialize the capture graph | AVFoundation, output services, device lookup/observers |
OutputService |
Normalize output, activity, capabilities, rotation, and reconfiguration | AVCaptureOutput |
PreviewSource / PreviewTarget |
Connect preview without exposing capture internals to SwiftUI | AVCaptureSession only at the target boundary |
Access control
| Symbol | Access | Verified effect | Likely rationale |
|---|---|---|---|
CameraModel.status, activity, readiness, thumbnail, error |
private(set) |
Views can read state but only the model can mutate it | Preserves the model as the single UI-state writer. |
CameraModel.mediaLibrary and captureService |
private let |
Neither dependency can be replaced or accessed by views | Keeps service lifecycle and commands behind the Camera interface. |
CaptureService.captureSession, output services, and inputs |
private |
Capture graph mutation stays inside the actor | Protects ordering and actor/queue confinement. |
CaptureService.previewSource |
nonisolated let, implicit internal |
Clients can connect a preview without awaiting actor isolation | Exposes a narrow immutable bridge, not the session itself. |
| App-defined types/protocols | implicit internal |
Shared only among targets/files in the built module | This is an app sample, not a public framework API. |
Reference code
AVCam/CameraModel.swift:24 — read-only view state
private(set) var status = CameraStatus.unknown
private(set) var captureActivity = CaptureActivity.idle
private(set) var thumbnail: CGImage?
private(set) var error: Error?No public, open, or fileprivate declaration appears in the AVCam Swift sources.
Logic ownership and placement
| Logic | Owning type or file | Placement rationale |
|---|---|---|
| View composition and gestures | CameraView and Views/ |
SwiftUI-only layout and user interactions. |
| UI state and intent translation | CameraModel |
Main-actor observable boundary consumed by views. |
| Session/device configuration | CaptureService |
Serialized off-main AVFoundation ownership. |
| Photo/movie mechanics | PhotoCapture, MovieCapture |
One object per output capability and delegate lifecycle. |
| Photos authorization/save/thumbnail | MediaLibrary |
Separate actor and framework boundary. |
| Persistent capture preferences | CameraState |
Codable state shared with the capture extension. |
Design patterns
| Pattern | Source evidence | Purpose or tradeoff |
|---|---|---|
| MVVM-style mediation | AVCam/CameraModel.swift:13 and class comment |
Keeps SwiftUI independent of capture implementation. |
| Protocol-oriented substitution | AVCam/Model/Camera.swift:13 and PreviewCameraModel conformance |
Lets previews use a non-hardware camera model. |
| Actor-isolated service | AVCam/CaptureService.swift:12 and custom executor at line 73 |
Serializes blocking capture operations away from the main actor. |
| Output-service composition | AVCam/CaptureService.swift:35 and outputServices |
Shares capability/rotation handling while retaining specialized photo/movie objects. |
| Adapter/bridge | AVCam/Views/CameraPreview.swift:11 and UIViewRepresentable |
Hosts an AVFoundation preview layer inside SwiftUI. |
| Delegate-to-async adaptation | AVCam/Capture/PhotoCapture.swift:43 |
Wraps callback capture in a checked continuation. |
Main application flow
sequenceDiagram
actor User
participant View as CameraView
participant Model as CameraModel
participant Service as CaptureService
participant Photo as PhotoCapture
participant Library as MediaLibrary
User->>View: Tap shutter
View->>Model: capturePhoto()
Model->>Service: capturePhoto(features)
Service->>Photo: capturePhoto(features)
Photo-->>Model: Photo
Model->>Library: save(photo)
Library-->>Model: thumbnail stream update
Model-->>View: observable thumbnail/activity
Reference code
AVCam/CameraModel.swift:153 — photo flow coordinator
func capturePhoto() async {
do {
let photo = try await captureService.capturePhoto(with: currentPhotoFeatures)
try await mediaLibrary.save(photo: photo)
} catch {
self.error = error
}
}Naming conventions
- Types: role nouns (
CameraModel,CaptureService,MediaLibrary) and feature-specific services (PhotoCapture,MovieCapture). - Protocols: capability or endpoint nouns (
Camera,OutputService,PreviewSource,PreviewTarget). - Methods: user commands (
capturePhoto,toggleRecording,switchVideoDevices) and setup verbs (setUpSession,configureControls). - State: Boolean names use
is,should, orprefers; state snapshots use noun types such asCaptureActivityandCaptureCapabilities. - Files: primary type per file; folders group feature mechanics, model contracts, views, and extensions.
Architecture takeaways
- The main actor owns presentation state; a dedicated actor owns the AVFoundation graph and its serial executor.
- Views depend on a
Cameracapability, allowing Simulator/preview substitution without conditional UI architecture. - Output-specific delegates and state stay in
PhotoCaptureandMovieCapture, preventingCaptureServicefrom becoming the only implementation class. - A narrow preview-source protocol crosses the UI/capture boundary without publishing the capture session.
private(set)makes observable state readable while keeping mutation authority explicit.
Source map
| Source file | Relevant symbols |
|---|---|
AVCam/AVCamApp.swift |
AVCamApp, composition root |
AVCam/CameraModel.swift |
CameraModel, UI state and intent coordination |
AVCam/CaptureService.swift |
CaptureService, capture graph ownership |
AVCam/Model/Camera.swift |
Camera protocol |
AVCam/Model/DataTypes.swift |
OutputService, state/value types |
AVCam/Capture/PhotoCapture.swift |
PhotoCapture, delegate-to-async bridge |
AVCam/Capture/MovieCapture.swift |
MovieCapture |
AVCam/Model/MediaLibrary.swift |
MediaLibrary |
AVCam/Views/CameraPreview.swift |
Preview protocols and SwiftUI/UIKit adapter |