Supporting Center Stage front camera in your iOS app
At a glance
| Item | Summary |
|---|---|
| Purpose | Capture photos and movies while applying Center Stage and smart-framing recommendations. |
| App architecture | SwiftUI views depend on a Camera protocol; observable CameraModel mediates UI state; a custom-executor CaptureService actor owns AVFoundation. |
| Main patterns | MVVM, actor-isolated service, protocol-oriented substitution, output-service strategies, AsyncStream observation. |
| Project style | Feature folders split Capture, Model, Views, and Support, with smaller toolbar/overlay view groups. |
Project structure
CenterStageFrontCamera/CenterStageFrontCam/
├── CenterStageFrontCam.swift
├── CameraModel.swift
├── CaptureService.swift
├── Capture/
│ ├── DeviceLookup.swift
│ ├── PhotoCapture.swift
│ └── MovieCapture.swift
├── Model/
│ ├── Camera.swift
│ ├── CameraState.swift
│ └── DataTypes.swift
├── Views/
│ ├── CameraUI.swift
│ ├── Controls/
│ ├── Overlays/
│ └── Toolbars/
└── Support/
The source separates observable app state from capture mechanics. PreviewCameraModel is a protocol-compatible fake kept under Preview Content.
Overall architecture
flowchart LR
App["CenterStageFrontCam"] --> View["CameraView / CameraUI"]
View --> CameraAPI["Camera protocol"]
CameraAPI --> Model["CameraModel @Observable"]
Model --> Service["CaptureService actor"]
Model --> Library["MediaLibrary"]
Service --> Session["AVCaptureSession"]
Service --> Photo["PhotoCapture"]
Service --> Movie["MovieCapture"]
Service --> Monitor["Smart framing monitor"]
Reference code
CenterStageFrontCamera/CenterStageFrontCam/CameraModel.swift:12 — the source explicitly defines the mediator boundary (abridged).
@Observable
final class CameraModel: Camera {
// ...
}Calling this MVVM is an interpretation, but it is strongly supported by this declared mediation role and the model-driven generic views.
Ownership and state
classDiagram
CenterStageFrontCam *-- CameraModel
CameraModel *-- CaptureService
CameraModel *-- MediaLibrary
CameraModel *-- CameraState
CaptureService *-- AVCaptureSession
CaptureService *-- PhotoCapture
CaptureService *-- MovieCapture
CameraUI --> CameraModel : observes via Camera
Ownership evidence
CenterStageFrontCamera/CenterStageFrontCam/CameraModel.swift:23 — read-only UI state and private owned collaborators (abridged).
private(set) var status = CameraStatus.unknown
private(set) var captureActivity = CaptureActivity.idle
private(set) var thumbnail: CGImage?
private let mediaLibrary = MediaLibrary()
private let captureService = CaptureService()
private var cameraState = CameraState()| Owner | Object or state | Relationship | Mutation authority |
|---|---|---|---|
CenterStageFrontCam |
CameraModel |
@State app-lifetime owner |
App creates; CameraModel mutates observable state. |
CameraModel |
UI-facing status/activity/settings | Main-actor observable state | Model methods and property observers update it. |
CaptureService |
Session, inputs, output services, smart-framing observation | Actor-owned capture graph | Actor serializes mutations on its custom session-queue executor. |
PhotoCapture / MovieCapture |
Concrete output and delegate lifecycle | Owned strategies | Each output service mutates its own capture-specific state. |
MediaLibrary |
Photo-library writes and thumbnail stream | Owned collaborator | Library performs persistence; model forwards thumbnail state. |
Class and protocol design
classDiagram
class Camera {
<<protocol>>
start()
capturePhoto()
toggleRecording()
}
CameraModel ..|> Camera
PreviewCameraModel ..|> Camera
class OutputService {
<<protocol>>
output
updateConfiguration()
}
PhotoCapture ..|> OutputService
MovieCapture ..|> OutputService
CaptureService --> OutputService
Reference code
CenterStageFrontCamera/CenterStageFrontCam/Model/Camera.swift:11 and CenterStageFrontCamera/CenterStageFrontCam/Model/DataTypes.swift:170 — UI substitution and capture-output capabilities are separate protocols (abridged).
@MainActor
protocol Camera: AnyObject, SendableMetatype {
var status: CameraStatus { get }
var previewSource: PreviewSource { get }
func start() async
func capturePhoto() async
func toggleRecording() async
}
protocol OutputService {
associatedtype Output: AVCaptureOutput
nonisolated var output: Output { get }
nonisolated func updateConfiguration(for device: AVCaptureDevice)
}The first protocol makes views previewable without hardware. The second lets CaptureService configure heterogeneous photo/movie outputs through shared behavior.
Access control
| Symbol | Access | Verified effect | Likely rationale |
|---|---|---|---|
CameraModel.status, activity, thumbnail |
private(set) |
Views can observe but cannot forge capture results. | Keeps mutation authority in the mediator. |
CameraModel.captureService |
private |
UI and other files cannot bypass the model. | Maintains one UI-to-capture command path. |
CaptureService.captureSession, outputs |
private |
Only the actor configures the capture graph. | Protects AVFoundation ordering and actor invariants. |
Activity streams and previewSource |
nonisolated let |
Consumers can obtain immutable handles without an actor hop. | Safe publication of fixed endpoints. |
| Selected AVFoundation references | nonisolated(unsafe) |
Compiler isolation checking is explicitly bypassed for non-Sendable framework objects. | Interop escape hatch; not general public access. |
CenterStageFrontCamera/CenterStageFrontCam/CaptureService.swift:13 (abridged):
actor CaptureService {
private(set) var captureActivity: CaptureActivity = .idle
nonisolated let captureActivityStream: AsyncStream<CaptureActivity>
nonisolated let previewSource: PreviewSource
private let captureSession = AVCaptureSession()
private let photoCapture = PhotoCapture()
private let movieCapture = MovieCapture()
}Logic ownership and placement
| Logic | Owning type or file | Placement rationale |
|---|---|---|
| UI state and intent mediation | CameraModel |
Translates view actions into async service calls and publishes results. |
| Session/device/smart-framing mutation | CaptureService |
Confines non-thread-safe AVFoundation operations to one actor executor. |
| Photo and movie details | PhotoCapture, MovieCapture |
Each service owns its output and framework delegate lifecycle. |
| Photos persistence | MediaLibrary |
Keeps system library access outside capture setup. |
| View composition | Views/ |
Generic views require only the Camera capability. |
Design patterns
| Pattern | Source evidence | Purpose or tradeoff |
|---|---|---|
| MVVM | CenterStageFrontCamera/CenterStageFrontCam/CameraModel.swift:12 |
Observable mediator separates SwiftUI from capture implementation; label is an inference. |
| Actor/service | CenterStageFrontCamera/CenterStageFrontCam/CaptureService.swift:13 |
Serializes capture graph changes off the main actor. |
| Protocol-oriented substitution | CenterStageFrontCamera/CenterStageFrontCam/Model/Camera.swift:11 |
Real and preview models share a view contract. |
| Strategy | CenterStageFrontCamera/CenterStageFrontCam/Model/DataTypes.swift:170 |
Photo/movie output services share configuration behavior. |
| Async sequence observation | CenterStageFrontCamera/CenterStageFrontCam/CameraModel.swift:198 |
Model consumes activity, thumbnail, and control-state streams without exposing continuations. |
Naming conventions
- Protocols name capabilities (
Camera,OutputService,PreviewSource,PreviewTarget). - Concrete types use responsibility nouns (
CaptureService,MediaLibrary,DeviceLookup). - Commands are verbs (
start,capturePhoto,toggleRecording,applyRecommendedFraming). - State uses
is/should/prefers;private(set)marks externally readable results. - Files generally match their primary type; view subfolders group Controls, Overlays, and Toolbars.
Architecture takeaways
- Put the observable main-actor model between SwiftUI and the actor-owned capture graph.
- Use capability protocols where there is a real substitute: previews and heterogeneous capture outputs.
- Publish immutable streams/preview endpoints from the actor; retain mutation inside it.
- Keep photo/movie capture and Photos persistence as distinct owners.
Source map
| Source file | Relevant symbols |
|---|---|
CenterStageFrontCamera/CenterStageFrontCam/CenterStageFrontCam.swift |
App ownership/startup |
CenterStageFrontCamera/CenterStageFrontCam/Model/Camera.swift |
UI-facing protocol |
CenterStageFrontCamera/CenterStageFrontCam/CameraModel.swift |
Observable mediator |
CenterStageFrontCamera/CenterStageFrontCam/CaptureService.swift |
Actor/session/smart framing |
CenterStageFrontCamera/CenterStageFrontCam/Model/DataTypes.swift |
OutputService |
Source caveat: the catalog marks the required iOS, iPadOS, Mac Catalyst, and Xcode 27 APIs as beta at review time.