Sample CodeiOS, iPadOS, Mac Catalyst, macOS, visionOSReviewed 2026-07-21View on Apple Developer

Processing spatial video with a custom video compositor

At a glance

Item Summary
Purpose Preview and export mono or stereo transformations of spatial video using custom video compositors.
App architecture SwiftUI owns one observable feature model; the model builds compositions and selects exporters through explicit protocol/factory boundaries.
Main patterns Observable model, Strategy/Factory, Builder, instruction decorator, and AVFoundation protocol implementations.
Project style Feature folders separate Compositing, Export, Model, and Views; files match their primary type.

Project structure

AVFSpatialCustomVideoCompositorSample/
├── AVFSpatialCustomVideoCompositorSample.swift # App root
├── ContentView.swift                           # Feature UI and file flow
├── SampleModel.swift                           # Playback/export owner
├── Model/UserSettings.swift                    # UserDefaults-backed selection
├── Compositing/
│   ├── VideoCompositionBuilder.swift
│   ├── MonoOutputCompositor.swift
│   ├── StereoOutputCompositor.swift
│   └── PixelBufferHelper.swift
├── Export/
│   ├── Exporter.swift                          # Protocol, factory enum, status/errors
│   ├── ExportSessionExporter.swift
│   ├── ReaderWriterExporter.swift
│   └── FileManagerExtensions.swift
└── Views/PlayerView.swift / SettingsView.swift

Structure observations

  • Folder boundaries follow capabilities rather than UI screens.
  • SampleModel is the feature facade and only long-lived owner of playback/export state.
  • Export has an app-defined protocol; compositing implements Apple’s protocol and uses an enum to choose concrete classes.

Overall architecture

Reference code

AVFSpatialCustomVideoCompositorSample/SampleModel.swift:80 — model selects export and composition collaborators

func export(using exporterType: ExporterType) async throws {
    exporter = exporterType.createExporter()
    isExportInProgress = true
    let asset = AVURLAsset(url: movieFile)
    let videoComposition = try await makeVideoComposition(for: asset)
    try await exporter?.export(asset: asset, videoComposition: videoComposition)
    // publish output, then release exporter
}

Interpretation

ContentView owns presentation state and sends intents to SampleModel. The model is a concrete observable facade: it owns the player, turns settings into a video composition, and retains the chosen exporter only for the active operation.

Ownership and state

Ownership evidence

AVFSpatialCustomVideoCompositorSample/ContentView.swift:14 and AVFSpatialCustomVideoCompositorSample/SampleModel.swift:14 — view/model ownership

@State private var sampleModel = SampleModel()

private(set) var player = AVPlayer()
private var movieFile: URL? = nil
private var exporter: Exporter?
private(set) var isExportInProgress = false
private(set) var exportTempURL: URL? = nil
Owner Object or state Relationship Mutation authority
ContentView SampleModel and modal/import/export presentation flags Creates as @State View for presentation; model for feature state
SampleModel Player, security-scoped movie URL, export state, active exporter Creates/replaces/retains SampleModel methods only for restricted properties
VideoCompositionBuilder Asset and compositor selection Receives immutable dependencies per build Builder operation
UserSettings.shared UserDefaults key/value Singleton value wrapper Computed property setter
AVFoundation Concrete custom compositor instance Instantiates from configured class Framework request lifecycle

Class and protocol design

Reference code

AVFSpatialCustomVideoCompositorSample/Export/Exporter.swift:27 — protocol factory

func createExporter() -> Exporter {
    switch self {
    case .exportSession: return ExportSessionExporter()
    case .readerWriter:  return ReaderWriterExporter()
    }
}

protocol Exporter {
    var status: ExporterStatus { get }
    func export(asset: AVAsset, videoComposition: AVVideoComposition?) async throws
}
Type Responsibility Depends on
SampleModel Coordinate security-scoped loading, playback composition, export, and observable state AVFoundation, settings, exporter abstraction
VideoCompositionBuilder Preserve color/projection/spatial configuration and select compositor class AVFoundation
MonoOutputCompositor / StereoOutputCompositor Process mono or tagged stereo source buffers AVVideoCompositing, pixel helpers
SpatialVideoCompositionInstruction Wrap a framework instruction while adding spatial configuration and projection tag AVVideoCompositionInstructionProtocol
Exporter implementations Export through high-level session or low-level reader/writer pipeline Exporter, AVFoundation

Access control

Symbol Access Verified effect Likely rationale
SampleModel.player, loaded/export flags, output URL, stereo-preview flag private(set) Views can observe values but only SampleModel can assign them. Enforces the model as mutation authority.
movieFile, active exporter, makePlayer, makeVideoComposition private UI cannot bypass security-scope, composition, or export lifecycle. Protects feature invariants behind commands.
Exporter status implementations private(set) Model can read progress/result but exporters alone transition status. Gives each exporter ownership of its state machine.
Reader/writer AVFoundation objects and helper methods private No caller can partially configure the low-level pipeline. Keeps reader/writer ordering internal.
UserSettings initializer, defaults object, and key private Callers must use shared and the typed property. Centralizes persistence key and default behavior.
Builder asset, compositor type, and derived async helpers private Only build() exposes composition construction. Presents a narrow builder API.
App protocols/types otherwise internal by default Reusable across this app target, not exported as a framework. Appropriate application boundary.

Reference code

AVFSpatialCustomVideoCompositorSample/SampleModel.swift:14 and AVFSpatialCustomVideoCompositorSample/Model/UserSettings.swift:10 — enforced mutation and singleton boundary

private(set) var player = AVPlayer()
private(set) var isMovieFileLoaded = false
private var exporter: Exporter?

struct UserSettings {
    static let shared = UserSettings()
    private let userDefaults = UserDefaults.standard
    private init() {}
}

Logic ownership and placement

Logic Owning type or file Placement rationale
Import UI, modal state, and file movement ContentView Platform presentation concerns
Security-scoped URL and player replacement SampleModel Long-lived feature lifecycle
Translate setting into composition SampleModel.makeVideoComposition Joins user preference to builder invocation
Color/spatial configuration and instruction wrapping VideoCompositionBuilder Composition-construction concern
Pixel transformations compositor types and PixelBufferHelper Per-frame AVFoundation callback path
Export state and mechanics exporter implementations Each strategy owns its AV resources and progress

Design patterns

Pattern Source evidence Purpose or tradeoff
Observable feature model AVFSpatialCustomVideoCompositorSample/SampleModel.swift:11, AVFSpatialCustomVideoCompositorSample/ContentView.swift:14 Keeps SwiftUI declarative while centralizing playback/export state.
Strategy + Factory AVFSpatialCustomVideoCompositorSample/Export/Exporter.swift:11, AVFSpatialCustomVideoCompositorSample/Export/Exporter.swift:28 UI chooses an enum; model depends on the common Exporter contract.
Builder AVFSpatialCustomVideoCompositorSample/Compositing/VideoCompositionBuilder.swift:10, AVFSpatialCustomVideoCompositorSample/Compositing/VideoCompositionBuilder.swift:26 Constructs a correctly tagged/color-configured composition in one operation.
Decorator AVFSpatialCustomVideoCompositorSample/Compositing/VideoCompositionBuilder.swift:113 Forwards the framework instruction contract and attaches spatial metadata.
Framework strategy AVFSpatialCustomVideoCompositorSample/Compositing/VideoCompositionBuilder.swift:153 Supplies mono or stereo AVVideoCompositing classes to AVFoundation.
Singleton settings wrapper AVFSpatialCustomVideoCompositorSample/Model/UserSettings.swift:10 Central typed access, with the usual global-state/testability tradeoff.

Naming conventions

  • Capability roles use precise suffixes: Builder, Exporter, Compositor, Instruction, and Helper.
  • Concrete strategies state mechanism/output: ExportSessionExporter, ReaderWriterExporter, MonoOutputCompositor, StereoOutputCompositor.
  • Boolean names state conditions: isMovieFileLoaded, isExportInProgress, previewRequiresStereo, outputsStereo.
  • Commands use clear verbs: loadMovieFile, applyUserSettings, createExporter, build, and export.
  • Files mirror primary types and capability folders.

Architecture takeaways

  • Use private(set) when views need observation but one model must retain mutation authority.
  • A small protocol plus enum factory cleanly supports materially different export mechanisms.
  • Wrap framework instructions when custom compositors need metadata beyond the base protocol.
  • Keep video-composition construction separate from both UI and frame processing.
  • This sample demonstrates meaningful protocol-oriented design at both app (Exporter) and framework (AVVideoCompositing) boundaries.

Source map

Source file Relevant symbols
AVFSpatialCustomVideoCompositorSample/ContentView.swift view-owned model and file/export UI
AVFSpatialCustomVideoCompositorSample/SampleModel.swift SampleModel
AVFSpatialCustomVideoCompositorSample/Model/UserSettings.swift UserSettings
AVFSpatialCustomVideoCompositorSample/Compositing/VideoCompositionBuilder.swift builder, instruction wrapper, CompositorType
AVFSpatialCustomVideoCompositorSample/Compositing/MonoOutputCompositor.swift mono compositor
AVFSpatialCustomVideoCompositorSample/Compositing/StereoOutputCompositor.swift stereo compositor
AVFSpatialCustomVideoCompositorSample/Export/Exporter.swift protocol, factory enum, status, errors
AVFSpatialCustomVideoCompositorSample/Export/ExportSessionExporter.swift high-level exporter
AVFSpatialCustomVideoCompositorSample/Export/ReaderWriterExporter.swift low-level exporter