Sample CodemacOS 13.0+Reviewed 2026-07-21View on Apple Developer

Supporting Continuity Camera in your macOS app

At a glance

Item Summary
Purpose Use an iPhone as a macOS camera/microphone, follow system-preferred device changes, and expose supported video effects.
App architecture ContentView owns one main-actor Camera observable object that combines capture-session ownership, device selection, and UI-facing state.
Main patterns MVVM-style observable model, Combine/KVO observers, NSViewRepresentable preview adapter.
Project style Small flat model/observer layer plus a Views folder; no separate service or protocol abstraction.

Project structure

ContinuityCam/
├── ContinuityCamApp.swift
├── ContentView.swift
├── Camera.swift
├── DeviceObservers.swift
└── Views/
    ├── CameraPreview.swift
    ├── ConfigurationView.swift
    └── MaterialView.swift

Camera.swift includes its supporting Device and VideoFormat value types and format extension. The split is by UI, camera model, and observation concern rather than a deep layer hierarchy.

Overall architecture

Reference code

ContinuityCam/ContentView.swift:10 — the root view creates one camera model and supplies its preview/configuration projections (abridged).

struct ContentView: View {
    @StateObject var camera = Camera()

    var body: some View {
        HStack(spacing: 0) {
            camera.preview
                .onAppear { Task { await camera.start() } }
            ConfigurationView(camera: camera)
        }
    }
}

The source combines model and service responsibilities in Camera. “MVVM-style” is an inference from a view-owned observable model, not an explicit project label.

Ownership and state

Ownership evidence

ContinuityCam/Camera.swift:29 — session and observer ownership sits inside Camera (abridged).

private(set) var isSetup = false
private(set) var isAuthorized = false
private(set) var isRunning = false
private let session = AVCaptureSession()
private let preferredCameraObserver = PreferredCameraObserver()
private let videoEffectsObserver = VideoEffectsObserver()
private var subscriptions = Set<AnyCancellable>()
Owner Object or state Relationship Mutation authority
ContentView Camera @StateObject, retained for view lifetime Camera mutates its published state.
Camera Capture session and inputs Creates and reconfigures Private setup/device-selection methods.
Camera Observer objects and subscriptions Creates and retains Observers publish KVO changes; Camera maps them into UI state.
CameraPreview Preview layer Adapter receives the private session Nested CaptureVideoPreview configures its backing layer once.

Class and protocol design

Reference code

ContinuityCam/Camera.swift:97 — KVO bridge objects feed the observable model through Combine (abridged).

preferredCameraObserver.$systemPreferredCamera
    .dropFirst().removeDuplicates().compactMap({ $0 })
    .sink { [weak self] captureDevice in
        guard let self = self, self.isSetup else { return }
        Task { await self.systemPreferredCameraChanged(to: captureDevice) }
    }.store(in: &subscriptions)

videoEffectsObserver.$isCenterStageEnabled
    .receive(on: RunLoop.main)
    .assign(to: \.isCenterStageEnabled, on: self)
    .store(in: &subscriptions)

There is no app-defined protocol boundary. Instead, small observer classes bridge static AVCaptureDevice KVO into Combine, while CameraPreview bridges AppKit into SwiftUI.

Access control

Symbol Access Verified effect Likely rationale
Capture session, inputs, discovery sessions private Views cannot mutate the capture graph. Keeps configuration inside one owner.
state, device arrays, effect support @Published private(set) Views observe but cannot publish derived system state. Makes Camera the source of truth.
Selected device/format properties internal writable @Published Configuration UI can bind directly and trigger model subscriptions. Deliberate two-way UI intent boundary.
Observer KVO results @Published private(set) Only KVO callbacks update system-derived values. Prevents consumers from simulating framework state.
CameraPreview.session private let Session is only used during native-view construction. Limits the adapter to rendering.

ContinuityCam/DeviceObservers.swift:41:

class PreferredCameraObserver: NSObject, ObservableObject {
    private let systemPreferredKeyPath = "systemPreferredCamera"
    @Published private(set) var systemPreferredCamera: AVCaptureDevice?
}

Logic ownership and placement

Logic Owning type or file Placement rationale
Authorization, discovery, session input changes Camera One main-actor owner coordinates capture and published selections.
Preferred-camera/effect KVO DeviceObservers.swift Keeps Objective-C observation mechanics out of the model’s main flow.
Device/format controls ConfigurationView SwiftUI binds to the model’s explicit writable selections.
AppKit preview layer CameraPreview NSViewRepresentable isolates native layer setup.

Design patterns

Pattern Source evidence Purpose or tradeoff
Observable model ContinuityCam/Camera.swift:12 One source of UI-facing capture state; also carries service responsibilities.
Observer bridge ContinuityCam/DeviceObservers.swift:20 Converts static KVO properties into typed published values.
Adapter ContinuityCam/Views/CameraPreview.swift:10 Embeds an AVCaptureVideoPreviewLayer in SwiftUI.
Reactive intent ContinuityCam/Camera.swift:109 Published selections trigger reconfiguration through subscriptions.

Naming conventions

  • Role nouns are direct: Camera, CameraPreview, ConfigurationView, PreferredCameraObserver.
  • Boolean state uses is...Enabled, is...Supported, isSetup, and isRunning.
  • Methods distinguish setup (setupInputs) from selection (selectCaptureDevice) and derived-state updates (updateVideoEffectsState).
  • Primary SwiftUI/AppKit adapters live under Views; model-supporting value types remain with Camera.

Architecture takeaways

  • A small sample can use one observable camera owner without a separate service layer when capture scope is narrow.
  • Use private(set) for system-derived UI state and writable published properties only for real user intent.
  • Isolate KVO and native-view bridging in small adapter objects/files.
  • The sample demonstrates preview and configuration; it does not implement photo/movie output persistence.

Source map

Source file Relevant symbols
ContinuityCam/ContentView.swift Root camera ownership
ContinuityCam/Camera.swift Session, published state, device selection
ContinuityCam/DeviceObservers.swift KVO-to-Combine bridges
ContinuityCam/Views/CameraPreview.swift SwiftUI/AppKit preview adapter
ContinuityCam/Views/ConfigurationView.swift User intent bindings