Sample CodetvOSReviewed 2026-07-21View on Apple Developer

Supporting Continuity Camera in your tvOS app

At a glance

Item Summary
Purpose Connects an iPhone or iPad to Apple TV and captures photos, video, and two-way audio.
App architecture The SwiftUI app is the composition root for one capture-session manager and three media-specific capturers; ContentView observes and commands them.
Main patterns Observable service objects, framework delegates, observer adapters, closure callback, and feature extensions.
Project style One tvOS target grouped by app entry, camera coordination, media export, observers, and reusable views.

Project structure

App/
└── ContinuityCameraApp.swift
Camera/
├── CaptureManager.swift
├── CaptureManager+ContinuityCamera.swift
├── CaptureManager+Observers.swift
└── CaptureType.swift
Media export/
├── AudioCapturer.swift
├── PhotoCapturer.swift
└── VideoCapturer.swift
Observers/
├── CaptureDeviceNotificationObserver.swift
├── PreferredCameraObserver.swift
└── VideoEffectsObserver.swift
Views/
├── ContentView.swift
├── CameraPreview.swift
├── AudioControls.swift
└── CaptureButton.swift

Structure observations

  • Folders follow responsibilities rather than generic MVC layers.
  • CaptureManager extensions separate device selection and observation mechanics without creating additional owners.
  • Each exported medium has a dedicated capturer, while the shared camera session remains centralized.

Overall architecture

Reference code

App/ContinuityCameraApp.swift:31 — the app root wires media outputs into the shared session, then injects every owner into the main view.

init() {
    captureManager.addOutput(photoCapturer.photoOutput)
    captureManager.addOutput(videoCapturer.movieFileOutput)
}

var body: some Scene {
    WindowGroup {
        ContentView(captureManager: captureManager,
                    photoCapturer: photoCapturer,
                    videoCapturer: videoCapturer,
                    audioCapturer: audioCapturer)
    }
}

Interpretation

The view composes the UI but does not construct capture infrastructure. Camera input and session configuration belong to CaptureManager; media-specific capture, export, and published state belong to the corresponding capturer.

Ownership and state

Ownership evidence

Camera/CaptureManager.swift:22 — the manager creates the session, derives its preview layer from that session, and retains observation adapters.

let session = AVCaptureSession()
let previewLayer: AVCaptureVideoPreviewLayer

var videoEffectsObvserver = VideoEffectsObserver()
var notificationObserver = CaptureManager.makeNotificationObserver()
var preferredCameraObserver = PreferredCameraObserver()

override init() {
    previewLayer = AVCaptureVideoPreviewLayer(session: session)
    super.init()
    preferredCameraObserver.handler = updateSystemPreferredCamera(_:)
}
Owner Object or state Relationship Mutation authority
App root Manager and three capturers Creates, retains, and injects Each service mutates its own state
CaptureManager Session, active input, preview layer, observers, isActive Creates and coordinates Manager and its same-type extensions
PhotoCapturer Photo output and completion closure Creates and retains Capturer/delegate callbacks; view installs completion
VideoCapturer Movie output and capture state Creates and retains Commands and recording-delegate callbacks
AudioCapturer Audio engine/session-facing state and recorded file Creates and retains Audio service methods and notifications
ContentView Mode, picker, flash, and preview UI state Locally owns with @State View actions only

Class and protocol design

Type or protocol Responsibility Depends on
CaptureManager Select inputs, configure the shared video session, publish connection state AVFoundation capture APIs and observers
PhotoCapturer / AVCapturePhotoCaptureDelegate Request still capture, normalize orientation, and save to Photos Photo output and photo-library API
VideoCapturer / AVCaptureFileOutputRecordingDelegate Start/stop movie output and save completed recordings Capture manager, movie output, Photos
AudioCapturer Configure voice processing, record, and replay audio AVAudioEngine and AVAudioSession
Observer types Translate KVO or notifications into typed handlers/published state AVFoundation and NotificationCenter
ContentView Choose capture mode and dispatch user intent Four observable service objects

The sample uses protocol orientation at framework boundaries through AVFoundation delegate conformances. It favors concrete app services and small typed closures over app-defined service protocols.

Access control

Symbol Access Verified effect Likely rationale
CaptureManager.activeInput explicit internal Same-module files and CaptureManager extensions can replace the active device. Device-disconnection logic in another file needs mutation access.
Preferred-camera update and authorization helper private Only CaptureManager.swift can invoke these implementation details directly. Keep permission and callback plumbing behind manager commands.
AudioCapturer engine, session, nodes, and recorded URL private Views cannot rewire the audio graph or storage location. Protect audio-engine lifecycle invariants.
Audio published properties private(set) Views observe state but cannot assign it. Maintain a one-way command/state boundary.
Capturer commands and output handles public Other same-target files can start work and attach outputs; the enclosing classes are still implicit internal. Mark intended collaborator-facing operations in the sample, not a public framework API.
ContentView transient state and helpers mostly private Capture mode UI details do not become module-wide symbols. Keep view implementation local.

Reference code

Camera/CaptureManager.swift:33activeInput is intentionally module-visible, but it centralizes the remove/add side effects in one property.

internal var activeInput: AVCaptureDeviceInput? {
    willSet {
        if let oldInput = activeInput { session.removeInput(oldInput) }
    }
    didSet {
        if let newInput = activeInput { session.addInput(newInput) }
        isActive = (activeInput != nil)
    }
}

Media export/AudioCapturer.swift:27 — mutable engine internals are private, while UI-facing state is read-only outside the capturer.

private let avAudioEngine = AVAudioEngine()
private let avAudioSession = AVAudioSession.sharedInstance()

@Published private(set) var playingBackgroundNoise = false
@Published private(set) var canPlayRecordedAudio = false
@Published private(set) var isPlayingRecordedAudio = false

Logic ownership and placement

Logic Owning type or file Placement rationale
Session input/output configuration and permission CaptureManager Shared AV capture authority.
Default Continuity Camera discovery CaptureManager+ContinuityCamera.swift Feature-specific extension of the same owner.
KVO and device-notification routing Observer types plus CaptureManager+Observers.swift Isolates framework observation mechanics from core session commands.
Still/movie/audio capture and export Corresponding capturer Keeps medium-specific APIs and state independent.
Mode selection and shutter dispatch ContentView User-intent translation belongs at the presentation boundary.
Photo preview and flash ContentView Transient visual feedback is view state, not capture state.

Design patterns

Pattern Source evidence Purpose or tradeoff
Composition root App/ContinuityCameraApp.swift:12 Makes object creation and cross-service wiring visible in one place.
Observable service object Camera/CaptureManager.swift:18 SwiftUI observes long-lived framework coordination without owning it.
Specialized services Media export/PhotoCapturer.swift:13 and Media export/VideoCapturer.swift:13 Separates capture/export lifecycles by medium.
Observer adapter Observers/PreferredCameraObserver.swift:16 Encapsulates KVO registration/removal and emits a typed camera callback.
Framework delegate Media export/VideoCapturer.swift:84 Keeps asynchronous recording completion with the object that owns the output.
Extension-based decomposition Camera/CaptureManager+ContinuityCamera.swift:10 Splits feature mechanics without splitting state ownership.

Main application flow

Reference code

Views/ContentView.swift:209 — one UI action dispatches to the service that owns the selected medium.

private func onShutterButton() {
    switch captureType {
        case .photo:
            photoCapturer.onPhotoCompletion = { image in
                showScreenFlash = true
                previewImage = image
            }
            photoCapturer.capture()
        case .video:
            toggleVideoCapture()
        case .audio:
            toggleAudioCapture()
    }
}

Naming conventions

  • Owner types use role suffixes: CaptureManager, PhotoCapturer, VideoCapturer, and PreferredCameraObserver.
  • Commands use concrete verbs: activate, setActive, addOutput, startRecording, stopRecording, and writeToPhotoLibrary.
  • UI Boolean state uses is, can, and show; callbacks use the on...Completion form.
  • +Feature filenames identify extensions of the same owner, while folders name domain responsibilities.
  • Framework objects retain their Apple terminology (session, input, output, previewLayer) instead of introducing aliases.

Architecture takeaways

  • Construct long-lived capture collaborators at the app boundary and pass them into SwiftUI views as observed dependencies.
  • Keep one shared session owner, then give each media format a focused capture/export owner.
  • Hide audio and session mutation behind commands while exposing observable state read-only where practical.
  • Use small observer adapters to contain KVO and notification cleanup.
  • Split a large owner by feature extensions only when ownership and invariants remain unambiguous.

Source map

Source file Relevant symbols
App/ContinuityCameraApp.swift Composition root and output wiring
Camera/CaptureManager.swift Session, input, output, permission, and core ownership
Camera/CaptureManager+ContinuityCamera.swift Default Continuity Camera activation
Camera/CaptureManager+Observers.swift Camera observer coordination
Media export/PhotoCapturer.swift Photo delegate and photo-library export
Media export/VideoCapturer.swift Movie output, recording state, and export delegate
Media export/AudioCapturer.swift Audio engine and observable audio state
Views/ContentView.swift Capture-mode UI and command dispatch