Sample CodeiOS, iPadOS, Mac CatalystReviewed 2026-11-01View on Apple Developer

Supporting device rotation in your camera app

At a glance

Item Summary
Purpose Keep the camera preview and captured photos and movies upright by applying the angles that AVCaptureDevice.RotationCoordinator reports.
App architecture One Xcode project (AVCam.xcodeproj) with three targets. A SwiftUI app and a Locked Camera Capture extension each create a @MainActor CameraModel. The model owns a CaptureService actor that owns the AVCaptureSession, the output wrappers, and the rotation coordinator.
Main patterns Actor isolation (with a custom serial executor), protocol-oriented abstraction (Camera, OutputService, PreviewSource/PreviewTarget), delegate callbacks bridged to async, and @Published state consumed as async sequences.
Project style Swift sources grouped by role: Capture/, Model/, Views/, Support/, Preview Content/. CaptureService, CameraModel, CameraView, and the app entry sit at the AVCam/ root.
Execution model CameraModel and the views are @MainActor. CaptureService is an actor whose unownedExecutor is a DispatchSerialQueue named sessionQueue. Preview-rotation writes hop back with Task { @MainActor in ... }. Capture-rotation writes stay on the actor.
State/event model Key-value observation of the rotation coordinator, @Published properties read through .values async sequences, AsyncStream wrappers around KVO and Photos callbacks, and @Observable model state read by SwiftUI.
Key frameworks/packages AVFoundation, SwiftUI, Combine, Photos, LockedCameraCapture, AppIntents, WidgetKit. The project declares no Swift package.

Platform boundary: the DocC record lists iOS, iPadOS, and Mac Catalyst. In the downloaded project, every target sets SUPPORTS_MACCATALYST = NO and SDKROOT = iphoneos (AVCam.xcodeproj/project.pbxproj:945), so the source shows only an iPhone and iPad build. The project also sets an iOS 26.0 deployment target (AVCam.xcodeproj/project.pbxproj:936), while the DocC record lists 27.0.

Project structure

AVCam.xcodeproj/                     (3 targets, no Swift packages)
AVCam/                               app target sources (also compiled into the capture extension)
├── AVCamApp.swift                   @main App, creates CameraModel
├── CameraModel.swift                @MainActor @Observable model
├── CaptureService.swift             actor; session, devices, rotation coordinator
├── CameraView.swift                 root view, generic over Camera
├── Capture/
│   ├── PhotoCapture.swift           OutputService for AVCapturePhotoOutput
│   ├── MovieCapture.swift           OutputService for AVCaptureMovieFileOutput
│   ├── DeviceLookup.swift           discovery sessions
│   └── SPCObserver.swift            SystemPreferredCameraObserver (KVO -> AsyncStream)
├── Model/
│   ├── Camera.swift                 @MainActor protocol for the view model
│   ├── CameraState.swift            persisted state shared with the extension
│   ├── DataTypes.swift              enums, value types, OutputService protocol
│   ├── MediaLibrary.swift           actor; Photos saving and thumbnails
│   └── Intent/AVCamCaptureIntent.swift
├── Views/                           CameraPreview, PreviewContainer, CameraUI, toolbars, overlays
├── Support/                         extensions
└── Preview Content/PreviewCameraModel.swift
AVCamCaptureExtension/               LockedCameraCaptureExtension entry
AVCamControlCenterExtension/         ControlWidget bundle

Structure observations

  • The project has three native targets: AVCam (application), AVCamCaptureExtension (ExtensionKit extension), and AVCamControlCenterExtension (app extension) (AVCam.xcodeproj/project.pbxproj:445).
  • The capture extension’s Sources phase compiles the shared app files. This includes CaptureService.swift and CameraModel.swift but not AVCamApp.swift (AVCam.xcodeproj/project.pbxproj:525, AVCam.xcodeproj/project.pbxproj:542). That is how AVCamCaptureExtension can build its own CameraModel (AVCamCaptureExtension/AVCamCaptureExtension.swift:16).
  • All rotation logic is in one // MARK: - Rotation handling section of CaptureService (AVCam/CaptureService.swift:365). One protocol-extension method in DataTypes.swift applies the capture angle to each output (AVCam/Model/DataTypes.swift:157). No view type computes angles.
  • The interface-orientation settings differ by device family: iPhone supports only portrait, and iPad supports all four orientations (AVCam.xcodeproj/project.pbxproj:933, AVCam.xcodeproj/project.pbxproj:934). The view layer also defines its aspect ratios in portrait terms (AVCam/Views/PreviewContainer.swift:10).
  • This snapshot also includes AVCam features unrelated to rotation: a photo-dimension picker, photo-output readiness tracking, and a menu that toggles deferred processing, responsive capture, and fast capture (AVCam/CameraModel.swift:135, AVCam/Capture/PhotoCapture.swift:169). This document mentions them only where they affect ownership or concurrency.

Overall architecture

Reference code

AVCam/CameraModel.swift:20 — CameraModel

@MainActor
@Observable
final class CameraModel: Camera {
    // ...
    /// An object that provides the connection between the capture session and the video preview layer.
    var previewSource: PreviewSource { captureService.previewSource }
    // ...
    /// An object that saves captured media to a person's Photos library.
    private let mediaLibrary = MediaLibrary()
    
    /// An object that manages the app's capture functionality.
    private let captureService = CaptureService()
    // ...
}

Interpretation

Observed: the dependencies point one way. Views know only the Camera protocol and a PreviewSource. CameraModel privately owns CaptureService and MediaLibrary. CaptureService owns every AVFoundation object, including the rotation coordinator. The only session-related value that reaches the UI is previewSource, which CameraModel forwards from the actor’s nonisolated let (AVCam/CaptureService.swift:30).

Inference: keeping rotation inside the actor means the coordinator, the observers, and output-connection writes all live in the same place as the session reconfiguration that replaces them (changeCaptureDevice(to:)). The view layer never needs to know about device orientation.

Ownership and state

Ownership evidence

AVCam/CaptureService.swift:30 — CaptureService stored properties

actor CaptureService {
    // ...
    /// A type that connects a preview destination with the capture session.
    nonisolated let previewSource: PreviewSource
    
    // The app's capture session.
    private let captureSession = AVCaptureSession()
    
    // An object that manages the app's photo capture behavior.
    private let photoCapture = PhotoCapture()
    
    // An object that manages the app's video capture behavior.
    private let movieCapture = MovieCapture()
    
    // An internal collection of output services.
    private var outputServices: [any OutputService] { [photoCapture, movieCapture] }
    // ...
    // An object that monitors video device rotations.
    private var rotationCoordinator: AVCaptureDevice.RotationCoordinator!
    private var rotationObservers = [AnyObject]()
    // ...
}
Owner Object or state Relationship Mutation authority
AVCamApp CameraModel (camera) Creates it in @State private var (AVCam/AVCamApp.swift:16) CameraModel changes its own state; views call its methods
AVCamCaptureExtension A separate CameraModel Creates it in @State private var (AVCamCaptureExtension/AVCamCaptureExtension.swift:16) Same as above, inside the extension process
CameraModel CaptureService, MediaLibrary Creates and retains them in private let (AVCam/CameraModel.swift:64, AVCam/CameraModel.swift:67) Each actor changes its own isolated state
CameraModel status, captureActivity, thumbnail, isSwitchingVideoDevices, … private(set) observable properties (AVCam/CameraModel.swift:25) Only CameraModel, on the main actor
CaptureService AVCaptureSession, PhotoCapture, MovieCapture, DeviceLookup Creates them in private let Only the actor (its sessionQueue executor)
CaptureService rotationCoordinator, rotationObservers private var, replaced on every call to createRotationCoordinator(for:) (AVCam/CaptureService.swift:370) Only the actor
CaptureService Preview connection videoRotationAngle Reads the connection on the actor, then writes it inside Task { @MainActor in } (AVCam/CaptureService.swift:399) Main actor
PhotoCapture / MovieCapture Output connection videoRotationAngle Written by the OutputService extension (AVCam/Model/DataTypes.swift:159) that CaptureService calls Whatever context calls it; here that is the CaptureService actor
CameraView and child views camera reference Store the passed model in @State var camera (AVCam/CameraView.swift:18) They only read it or call its methods

Observed: CameraView stores the model in a @State property but does not create it. The object still comes from the app or extension entry point, so the view’s property is a shared reference, not a second owner. PreviewView uses AVCaptureVideoPreviewLayer as its backing layer class (AVCam/Views/CameraPreview.swift:54), so the UIKit view owns the layer, and the capture session is attached later through PreviewTarget.setSession(_:).

Concurrency, scheduling, and thread safety

Evidence limit: actor isolation, async/await, or Task creation does not by itself prove background-thread execution; Sendable conformance alone does not prove thread-safe mutation.

Concern Source mechanism Verified placement or handoff Evidence
Main-actor isolation @MainActor on CameraModel, the Camera protocol, and the views All model state that SwiftUI reads is main-actor isolated AVCam/CameraModel.swift:20, AVCam/Model/Camera.swift:15, AVCam/CameraView.swift:12
Actor executor actor CaptureService with nonisolated var unownedExecutor returning sessionQueue.asUnownedSerialExecutor() Actor-isolated work, including captureSession.startRunning() and every rotation update, runs as jobs on the serial dispatch queue sessionQueue. A queue is not a fixed thread. AVCam/CaptureService.swift:70, AVCam/CaptureService.swift:73
Same queue for framework callbacks setControlsDelegate(_:queue: sessionQueue) and setActionQueue(sessionQueue) Capture-control delegate callbacks and lens-slider actions are delivered on the queue that also serves as the actor’s executor AVCam/CaptureService.swift:218, AVCam/CaptureService.swift:237
KVO callback re-entry rotationCoordinator.observe(...) { [weak self] ... Task { await self.update...(angle) } } The KVO closure is not actor-isolated, so it starts a task and uses await to enter CaptureService. The observation closure weakly captures the actor. AVCam/CaptureService.swift:381, AVCam/CaptureService.swift:384, AVCam/CaptureService.swift:392
Preview hop to main actor Task { @MainActor in connection?.videoRotationAngle = angle } The actor reads videoPreviewLayer.connection, then writes the preview angle on the main actor AVCam/CaptureService.swift:398, AVCam/CaptureService.swift:399
Capture angle on the actor outputServices.forEach { $0.setVideoRotationAngle(angle) } Photo and movie output connections are updated synchronously inside the actor, with no hop AVCam/CaptureService.swift:407
Plain Task {} inheriting the actor Unretained tasks in monitorSystemPreferredCamera() and observeNotifications() Each body calls isolated members without await (changeCaptureDevice(to:), isInterrupted = ...), which shows it inherits CaptureService isolation. No property stores these tasks, so nothing cancels them. AVCam/CaptureService.swift:353, AVCam/CaptureService.swift:359, AVCam/CaptureService.swift:628
Retained, cancellable task subjectAreaChangeTask?.cancel(), then a new Task The only actor task that is stored and cancelled. It is replaced whenever the device changes. AVCam/CaptureService.swift:437, AVCam/CaptureService.swift:446
Retained KVO tokens rotationObservers.removeAll() with the comment “Cancel previous observations.” Clearing the array releases the previous coordinator’s observation tokens before new ones are added AVCam/CaptureService.swift:377
Plain Task {} inheriting the main actor CameraModel property didSet handlers and observeState() Each task mutates main-actor state directly and awaits actor members. The four for await loops in observeState() are not stored, so nothing cancels them. AVCam/CameraModel.swift:114, AVCam/CameraModel.swift:250
Caller-isolation inheritance monitorProgress(of:isolation: isolated (any Actor)? = #isolation) with _ = isolation in the Task The progress task captures the caller’s isolation (here the CaptureService actor, which calls PhotoCapture) rather than running nonisolated AVCam/Capture/PhotoCapture.swift:111, AVCam/Capture/PhotoCapture.swift:113
Main run-loop timer Timer.publish(every:on: .main, in: .common), stored in timerCancellable Recording-duration ticks are scheduled on the main run loop. stopMonitoringDuration() cancels the stored cancellable. AVCam/Capture/MovieCapture.swift:97, AVCam/Capture/MovieCapture.swift:109
SwiftUI-managed task .task { await camera.start() } SwiftUI ties the start-up task to the view’s lifetime. The app’s scenePhase handler separately creates Task { @MainActor in await camera.syncState() }. AVCam/AVCamApp.swift:25, AVCam/AVCamApp.swift:33
Preview attachment nonisolated func setSession(_:) creating Task { @MainActor in previewLayer.session = session } The preview layer’s session is assigned on the main actor AVCam/Views/CameraPreview.swift:62, AVCam/Views/CameraPreview.swift:65
Compiler checking level SWIFT_STRICT_CONCURRENCY = minimal, SWIFT_VERSION = 5.0 The app target builds in Swift 5 mode with minimal concurrency checking. Combined with @preconcurrency import AVFoundation, many of these crossings are not fully checked by the compiler. AVCam.xcodeproj/project.pbxproj:949, AVCam/CaptureService.swift:9

Reference code

AVCam/CaptureService.swift:70 — the actor’s custom serial executor

actor CaptureService {
    // ...
    // A serial dispatch queue to use for capture control actions.
    private let sessionQueue = DispatchSerialQueue(label: "com.example.apple-samplecode.AVCam.sessionQueue")
    
    // Sets the session queue as the actor's executor.
    nonisolated var unownedExecutor: UnownedSerialExecutor {
        sessionQueue.asUnownedSerialExecutor()
    }
    // ...
}

Interpretation: the custom executor is the specific source evidence that moves capture work off the main actor. actor alone would not establish that. The README states the goal directly: the actor exists “to ensure that AVFoundation capture API calls don’t occur on the main thread” (README.md:13). Because AVFoundation callbacks for controls use the same queue, those callbacks and actor jobs are serialized by one queue. The rotation KVO callbacks do not use that queue, which is why they re-enter through Task { await ... }.

Not established by source: which thread delivers the rotation coordinator’s KVO notifications, and whether MovieCapture.captureActivity, which the main-run-loop timer writes, is protected against the actor’s other accesses. The source shows no lock for that property.

State propagation, frameworks, and dependencies

Evidence limit: an import proves a source-level compilation dependency at the cited line; it does not prove runtime use, architectural adoption, or whether a Swift package is a direct application dependency.

Category Mechanism or module Verified role Evidence
Rotation signal KVO on videoRotationAngleForHorizonLevelPreview and videoRotationAngleForHorizonLevelCapture The coordinator’s two angle properties are the only rotation inputs. The app reads no device or interface orientation. AVCam/CaptureService.swift:381, AVCam/CaptureService.swift:389
Actor-to-model state @Published private(set) on the actor, read through await captureService.$captureActivity.values CameraModel loops over async sequences built from actor publishers AVCam/CaptureService.swift:17, AVCam/CameraModel.swift:259
Output-to-actor state Publishers.Merge(...).assign(to: &$captureActivity) Photo and movie activity publishers feed the actor’s own published property AVCam/CaptureService.swift:609
Callback to stream AsyncStream.makeStream in SystemPreferredCameraObserver, MediaLibrary, and PhotoCaptureDelegate KVO, PHImageManager callbacks, and photo-delegate events become async sequences AVCam/Capture/SPCObserver.swift:18, AVCam/Model/MediaLibrary.swift:27, AVCam/Capture/PhotoCapture.swift:220
Model-to-view state @Observable CameraModel SwiftUI views read model properties directly AVCam/CameraModel.swift:21
Persisted state CameraState property observers call save(), which starts a Task that updates the capture intent’s app context Shares capture mode and feature toggles with the extension AVCam/Model/CameraState.swift:34
Framework import @preconcurrency import AVFoundation, import Combine Capture APIs, plus @Published, Publishers.Merge, and .values AVCam/CaptureService.swift:9, AVCam/CaptureService.swift:10
Framework import Photos, UIKit Library saving and thumbnail creation AVCam/Model/MediaLibrary.swift:9
Framework import LockedCameraCapture, SwiftUI Capture-extension entry point AVCamCaptureExtension/AVCamCaptureExtension.swift:9
Package declaration None The project file declares no Swift package dependency AVCam.xcodeproj/project.pbxproj:445

Combine scheduling: the source never calls receive(on:) or subscribe(on:). Publisher values reach CameraModel through .values async sequences inside main-actor tasks. Observed: this is the only path by which actor-owned publishers update model state.

Class and protocol design

Reference code

AVCam/Model/DataTypes.swift:147 — OutputService and its default rotation method

protocol OutputService {
    associatedtype Output: AVCaptureOutput
    var output: Output { get }
    var captureActivity: CaptureActivity { get }
    var capabilities: CaptureCapabilities { get }
    func updateConfiguration(for device: AVCaptureDevice)
    func setVideoRotationAngle(_ angle: CGFloat)
}

extension OutputService {
    func setVideoRotationAngle(_ angle: CGFloat) {
        // Set the rotation angle on the output object's video connection.
        output.connection(with: .video)?.videoRotationAngle = angle
    }
    func updateConfiguration(for device: AVCaptureDevice) {}
}
Type Responsibility Depends on
CaptureService (actor) Session configuration, device switching, rotation coordinator lifetime, angle fan-out AVCaptureSession, OutputService conformers, DeviceLookup, SystemPreferredCameraObserver
CameraModel (@MainActor class) Turns UI intents into actor calls; mirrors actor state for SwiftUI CaptureService, MediaLibrary, CameraState
Camera (protocol) The view-facing model contract; lets previews use PreviewCameraModel Domain value types
PhotoCapture, MovieCapture Own one AVCaptureOutput each; adapt delegate callbacks to async OutputService
PreviewSource / PreviewTarget Connect a session to a preview view without exposing the session to view code AVCaptureSession
MediaLibrary (actor) Saves photos and movies to Photos; streams thumbnails Photos

Observed: OutputService declares setVideoRotationAngle(_:) as a requirement and provides it in an extension, so both outputs inherit one implementation. CaptureService rotates through [any OutputService] (AVCam/CaptureService.swift:42), so adding another output to that array would include it in rotation updates. Views are generic over CameraModel: Camera (AVCam/CameraView.swift:13), and PreviewCameraModel conforms to Camera (AVCam/Preview Content/PreviewCameraModel.swift:13).

Access control

Symbol Access Verified effect Likely rationale
CameraModel.captureService (AVCam/CameraModel.swift:67) private Views cannot reach the actor. They see only the forwarded previewSource. Inference: keeps capture operations behind the model’s API.
CameraModel.status and siblings (AVCam/CameraModel.swift:25) private(set) Readable module-wide; writable only inside CameraModel Inference: views observe status but cannot fake it.
CaptureService.captureActivity (AVCam/CaptureService.swift:17) @Published private(set) Other types can read the projected publisher but cannot assign the value Inference: the actor stays the single writer.
CaptureService.previewSource (AVCam/CaptureService.swift:30) implicit internal, nonisolated let Readable synchronously from the main actor without await Inference: lets CameraPreview connect the layer during view construction.
rotationCoordinator, rotationObservers, createRotationCoordinator(for:), updatePreviewRotation(_:) (AVCam/CaptureService.swift:57, AVCam/CaptureService.swift:368) private Rotation can be changed only from inside CaptureService Inference: rotation follows the session and device lifecycle, not UI events.
PhotoCaptureDelegate, ReadinessDelegate (AVCam/Capture/PhotoCapture.swift:202, AVCam/Capture/PhotoCapture.swift:180) private class Usable only within PhotoCapture.swift Inference: delegate bridging is an implementation detail.
MovieCaptureDelegate (AVCam/Capture/MovieCapture.swift:77) private class, nested Visible only inside MovieCapture Same as above.
DefaultPreviewSource.session (AVCam/Views/CameraPreview.swift:92) private let The session is reachable only through connect(to:) Matches the protocol comment about not exposing capture objects to the UI layer (AVCam/Views/CameraPreview.swift:76).

Reference code

AVCam/CaptureService.swift:368 — private rotation API

actor CaptureService {
    // ...
    /// Create a new rotation coordinator for the specified device and observe its state to monitor rotation changes.
    private func createRotationCoordinator(for device: AVCaptureDevice) {
        // Create a new rotation coordinator for this device.
        rotationCoordinator = AVCaptureDevice.RotationCoordinator(device: device, previewLayer: videoPreviewLayer)
        // ...
    }
    // ...
}

All types are in a single module, so most declarations use Swift’s implicit internal. The private and private(set) modifiers above are the boundaries that matter.

Logic ownership and placement

Logic Owning type or file Placement rationale
Create and replace the rotation coordinator CaptureService.createRotationCoordinator(for:) Called after setup (AVCam/CaptureService.swift:152) and after every device change (AVCam/CaptureService.swift:335), because the coordinator is tied to one device and one preview layer.
Find the preview layer CaptureService.videoPreviewLayer Derived from captureSession.connections, and calls fatalError if no preview layer is connected (AVCam/CaptureService.swift:412). Inference: this assumes the SwiftUI preview has attached its layer before setup finishes; the source shows no explicit synchronization for that ordering.
Apply the preview angle CaptureService.updatePreviewRotation(_:) Writes on the main actor, matching where the preview layer is attached.
Apply the capture angle OutputService.setVideoRotationAngle(_:) extension One shared implementation for every output connection.
Device switching CaptureService.changeCaptureDevice(to:) Bracketed by beginConfiguration and commitConfiguration, and re-creates the controls, rotation coordinator, and subject-area observation (AVCam/CaptureService.swift:319).
UI intent to capture call CameraModel Main-actor methods await the actor (AVCam/CameraModel.swift:126).
Layout PreviewContainer, CameraUI Aspect ratio and toolbar padding. CameraUI.bottomPadding reads UIScreen.main.bounds (AVCam/Views/CameraUI.swift:85).

Design patterns

Pattern Source evidence Purpose or tradeoff
Actor isolation actor CaptureService with a sessionQueue custom executor (AVCam/CaptureService.swift:14, AVCam/CaptureService.swift:73); actor MediaLibrary (AVCam/Model/MediaLibrary.swift:13) Serializes session, device, and rotation mutation on one queue, away from the main actor. KVO and main-actor work must hop explicitly.
Protocol-oriented abstraction Camera with CameraModel and PreviewCameraModel; OutputService with a default setVideoRotationAngle (AVCam/Model/DataTypes.swift:156); PreviewSource/PreviewTarget Swappable view models for previews, one rotation implementation for all outputs, and a session handoff that hides the session from views.
Delegate or data-source callbacks PhotoCaptureDelegate, MovieCaptureDelegate, ReadinessDelegate, CaptureControlsDelegate adopting AVFoundation delegate protocols (AVCam/CaptureService.swift:653) Framework callbacks are bridged into checked continuations, async streams, or @Published values.
Publisher-backed observable state @Published private(set) on CaptureService, PhotoCapture, MovieCapture, CaptureControlsDelegate, consumed through .values (AVCam/CameraModel.swift:259) The actor publishes, and the main-actor model iterates. Only the owner writes.

Removed from the draft: “Service object”. It came from the Service suffix. Here OutputService is a protocol for output wrappers, and CaptureService is better described by the verified actor-isolation and protocol boundaries above. No MVVM, Coordinator, or Adapter label is claimed: the source neither names those roles nor establishes them.

Main application flow

Reference code

AVCam/CaptureService.swift:368 — coordinator creation and observation

    private func createRotationCoordinator(for device: AVCaptureDevice) {
        // Create a new rotation coordinator for this device.
        rotationCoordinator = AVCaptureDevice.RotationCoordinator(device: device, previewLayer: videoPreviewLayer)
        
        // Set initial rotation state on the preview and output connections.
        updatePreviewRotation(rotationCoordinator.videoRotationAngleForHorizonLevelPreview)
        updateCaptureRotation(rotationCoordinator.videoRotationAngleForHorizonLevelCapture)
        
        // Cancel previous observations.
        rotationObservers.removeAll()
        
        // Add observers to monitor future changes.
        rotationObservers.append(
            rotationCoordinator.observe(\.videoRotationAngleForHorizonLevelPreview, options: .new) { [weak self] _, change in
                guard let self, let angle = change.newValue else { return }
                // Update the capture preview rotation.
                Task { await self.updatePreviewRotation(angle) }
            }
        )
        // ...
    }

AVCam/CaptureService.swift:397 — the two appliers

    private func updatePreviewRotation(_ angle: CGFloat) {
        let connection = videoPreviewLayer.connection
        Task { @MainActor in
            // Set initial rotation angle on the video preview.
            connection?.videoRotationAngle = angle
        }
    }
    
    private func updateCaptureRotation(_ angle: CGFloat) {
        // Update the orientation for all output services.
        outputServices.forEach { $0.setVideoRotationAngle(angle) }
    }

Observed: preview and capture angles are separate inputs with separate destinations. The preview angle goes to the preview layer’s connection, and the capture angle goes to every output’s video connection. Inference, consistent with Apple’s abstract: separating them lets the iPhone build, whose interface is portrait-only, keep a correct preview while photos and movies still record upright in any device orientation.

Naming conventions

  • Types: role nouns that pair a domain with a responsibility: CaptureService (actor), CameraModel (concrete model), PhotoCapture and MovieCapture (output wrappers), DeviceLookup, MediaLibrary, SystemPreferredCameraObserver. AVFoundation delegate adopters end in Delegate (PhotoCaptureDelegate, CaptureControlsDelegate). The protocol default implementation is DefaultPreviewSource, and the preview stub is PreviewCameraModel.
  • Protocols: Camera names the capability that views consume. OutputService, PreviewSource, and PreviewTarget name collaborator roles. The concrete class is named CameraModel, and views use CameraModel as their generic parameter name (CameraView<CameraModel: Camera>), which shadows the concrete type inside those views.
  • Methods: rotation verbs mirror the AVFoundation vocabulary: createRotationCoordinator(for:), updatePreviewRotation(_:), updateCaptureRotation(_:), setVideoRotationAngle(_:). Long-lived subscriptions use observe…/monitor… (observeNotifications, monitorSystemPreferredCamera, observeSubjectAreaChanges(of:)). Booleans use is…, prefers…, should… (isSwitchingVideoDevices, prefersMinimizedUI, shouldFlashScreen).
  • Files and extensions: generally one primary type per file, named after it. Exceptions: SPCObserver.swift holds SystemPreferredCameraObserver, and DataTypes.swift groups enums, value types, and the OutputService protocol. Folders group by role (Capture/, Model/, Views/Toolbars/...).

Architecture takeaways

  • A custom actor executor is the verified piece that puts capture work on a dedicated serial queue (AVCam/CaptureService.swift:73). Without it, actor would not say where the work runs.
  • Rotation is owned by the object that owns the device. The coordinator is rebuilt inside every device change, inside the same configuration transaction, so rotation state cannot drift from the active input.
  • Preview and capture rotation are separate data flows with different isolation: preview writes go to the main actor, and output writes stay on the actor.
  • A protocol extension (OutputService.setVideoRotationAngle) gives every output wrapper the same rotation behavior, and fan-out over [any OutputService] keeps CaptureService free of output-specific code.
  • Task lifetime is mostly implicit: one subject-area task is stored and cancelled, while the notification, preferred-camera, and model-observation loops are unretained. That is a deliberate reading point for anyone adapting this code.

Source map

Source file Relevant symbols
AVCam/CaptureService.swift CaptureService, sessionQueue, unownedExecutor, createRotationCoordinator(for:), updatePreviewRotation(_:), updateCaptureRotation(_:), videoPreviewLayer, changeCaptureDevice(to:), CaptureControlsDelegate
AVCam/Model/DataTypes.swift OutputService, setVideoRotationAngle(_:), CaptureActivity, CaptureCapabilities
AVCam/CameraModel.swift CameraModel, start(), observeState(), captureMode
AVCam/Model/Camera.swift Camera
AVCam/AVCamApp.swift AVCamApp
AVCamCaptureExtension/AVCamCaptureExtension.swift AVCamCaptureExtension
AVCam/CameraView.swift CameraView
AVCam/Views/CameraPreview.swift CameraPreview, PreviewView, PreviewSource, PreviewTarget, DefaultPreviewSource
AVCam/Views/PreviewContainer.swift PreviewContainer, photoAspectRatio, movieAspectRatio
AVCam/Views/CameraUI.swift CameraUI, bottomPadding
AVCam/Capture/PhotoCapture.swift PhotoCapture, monitorProgress(of:isolation:), ReadinessDelegate, PhotoCaptureDelegate
AVCam/Capture/MovieCapture.swift MovieCapture, MovieCaptureDelegate, startMonitoringDuration()
AVCam/Capture/SPCObserver.swift SystemPreferredCameraObserver
AVCam/Model/MediaLibrary.swift MediaLibrary, thumbnails
AVCam/Model/CameraState.swift CameraState, save()
AVCam/Preview Content/PreviewCameraModel.swift PreviewCameraModel
AVCam.xcodeproj/project.pbxproj Targets, Sources phases, interface orientations, SWIFT_STRICT_CONCURRENCY, SUPPORTS_MACCATALYST
README.md Rationale for the CaptureService actor