Sample CodemacOSReviewed 2026-07-21View on Apple Developer

Reading multiview 3D video files

At a glance

Item Summary
Purpose Open an MV-HEVC asset, validate it, and render the left- and right-eye image for a selected frame.
App architecture Small SwiftUI app with view-owned navigation state, a metadata view model, and a frame-decoding view model.
Main patterns MVVM, observable state, and an explicit loading/ready/error state machine.
Project style One app target; files are grouped by primary view or view-model type rather than feature folders.

Project structure

StereoViewer/
├── StereoViewerApp.swift          # Composition root
├── LaunchView.swift               # File selection and first navigation state
├── MediaDetailView.swift          # Asset-validation screen
├── MediaDetailViewModel.swift     # Async metadata loading
├── StereoView.swift               # Frame controls and image presentation
└── StereoViewModel.swift          # Track inspection and frame decoding

Structure observations

  • Each Swift file contains one primary type; view and view-model pairs share the same feature prefix.
  • The project has no separate service, domain, or persistence layer. AVFoundation work lives directly in the two view models.
  • Configuration/ and the Xcode project hold build configuration; they do not introduce runtime architecture.

Overall architecture

Reference code

StereoViewer/MediaDetailView.swift:18 — transition from validation to decoding

init(filename: URL) {
    mediaDetailViewModel = MediaDetailViewModel(filename: filename)
    self.filename = filename
}

// In body, after the user continues:
StereoView(asset: mediaDetailViewModel.asset)

Interpretation

The SwiftUI views form the navigation and composition layer. MediaDetailViewModel first validates and describes the selected asset; StereoViewModel then owns the lower-level AVFoundation read pipeline. This is a source-explicit MVVM organization, although the view models also act as framework services because the sample is intentionally small.

Ownership and state

Ownership evidence

StereoViewer/StereoView.swift:19 and StereoViewer/StereoViewModel.swift:26 — injected asset and owned decoder

init(asset: AVURLAsset) {
    self.asset = asset
    self.stereoViewModel = StereoViewModel(asset: asset)
}

private let asset: AVURLAsset
private var assetReader: AVAssetReader?
Owner Object or state Relationship Mutation authority
LaunchView Selected file URL Owns @State LaunchView actions only
MediaDetailView MediaDetailViewModel Creates and retains View model mutates metadata on the main actor
MediaDetailViewModel AVURLAsset and validation results Creates asset; publishes results View model only for mutable properties
StereoView Slider and local presentation state Owns @State StereoView only
StereoViewModel Track, timestamps, reader, output, and decoded images Retains or creates Decoder internals; image/state publication is main-actor isolated

The same AVURLAsset reference passes from the metadata phase into the decoding phase. The diagram uses aggregation for that injected reference and composition for objects each owner creates itself.

Class and protocol design

There are no application-defined protocols or interchangeable implementations. The design favors two concrete observable reference types because each coordinates mutable asynchronous framework state.

Type Responsibility Depends on
MediaDetailViewModel Load duration, playability, and readability AVURLAsset
StereoViewModel Discover the stereo track, index frames, configure a reader, and publish eye images AVFoundation, VideoToolbox, SwiftUI/AppKit image types
StereoViewModelState Represent loading, ready(times:), or error(message:) CMTime
LaunchView, MediaDetailView, StereoView File selection, flow decisions, and rendering Their owned state/view models

The ViewModel suffix, @Observable, and the direction of calls from views to model objects are direct evidence for MVVM. Calling the design protocol-oriented would not be supported by this source.

Access control

Symbol Access Verified effect Likely rationale
LaunchView.filename private Only LaunchView can read or mutate the selected URL. Keeps view navigation state local.
StereoView.stereoViewModel and UI state private Other files cannot replace the decoder or manipulate slider/presentation state. Protects view-owned lifecycle and UI invariants.
StereoViewModel.asset, reader, track, timestamps, and layer IDs private Consumers use commands and observable outputs instead of editing the read pipeline. Preserves reader configuration and frame-index consistency.
publishState, presentationTimesFor, loadVideoLayerIdsForTrack private Callable only inside their lexical type/file scope. Marks orchestration helpers as implementation details.
App types and view-model outputs without modifiers internal by default Available within the app module, not exported as a library API. Appropriate for a single-target sample app.

Reference code

StereoViewer/StereoViewModel.swift:22 — narrow public surface and private pipeline state

@MainActor var state: StereoViewModelState = .loading
@MainActor var leftEye = NSImage(...)
@MainActor var rightEye = NSImage(...)

private let asset: AVURLAsset
private var assetReader: AVAssetReader?
private var trackOutput: AVAssetReaderTrackOutput?

The three observable outputs remain module-visible so StereoView can read them, while the framework machinery is private. @MainActor controls concurrency isolation, not visibility. @unchecked Sendable is also a concurrency assertion, not an access level; the code manually routes published UI mutations to the main actor.

Logic ownership and placement

Logic Owning type or file Placement rationale
Choose a file and switch screens LaunchView Pure UI interaction and local navigation state
Load asset metadata MediaDetailViewModel Asynchronous state consumed by MediaDetailView
Frame controls and keyboard shortcuts StereoView Presentation-specific behavior
Track discovery and timestamp indexing StereoViewModel plus same-file private helpers Closely coupled to decode orchestration but hidden from consumers
Reader creation, seek range, and buffer decoding StereoViewModel The model owns all mutable AVFoundation reader state
Convert eye pixel buffers to images StereoViewModel Part of the decoding pipeline, with final image publication on MainActor

Design patterns

Pattern Source evidence Purpose or tradeoff
Model-View-ViewModel StereoViewer/MediaDetailView.swift:18, StereoViewer/StereoView.swift:19 Moves asynchronous AVFoundation work out of view bodies; concrete construction keeps the sample simple but tightly coupled.
Observable state StereoViewer/MediaDetailViewModel.swift:11, StereoViewer/StereoViewModel.swift:19 Lets SwiftUI update when metadata, state, or eye images change.
Explicit state machine StereoViewer/StereoViewModel.swift:12, StereoViewer/StereoView.swift:29 Makes loading, successful readiness, and errors exhaustive in the UI.
Encapsulated framework adapter StereoViewer/StereoViewModel.swift:65 Presents frame-oriented commands while hiding AVAssetReader setup and tagged-buffer parsing.

Main application flow

Reference code

StereoViewer/StereoViewModel.swift:39 and StereoViewer/StereoViewModel.swift:139 — track discovery to image publication

if let track = try await asset.loadTracks(
    withMediaCharacteristic: .containsStereoMultiviewVideo
).first {
    self.track = track
    self.framePresentationTimes = try presentationTimesFor(track: track, asset: asset)
    // ... configure and read the first frame
}

// For each tagged pixel buffer:
if tags.contains(.stereoView(.leftEye)) { leftEye = nsImage }
else if tags.contains(.stereoView(.rightEye)) { rightEye = nsImage }

Naming conventions

  • Types use feature-and-role names: MediaDetailView, MediaDetailViewModel, StereoView, and StereoViewModel.
  • State names describe lifecycle cases (loading, ready, error) and attach only the data relevant to a case.
  • Commands use verbs and reveal intent: readBufferFromAsset(at:), readNextBufferFromAsset(), and publishState(_:).
  • Loader/helper names describe returned data: presentationTimesFor(track:asset:) and loadVideoLayerIdsForTrack(_:).
  • Files match their primary type. The two file-private free functions stay beside the only consumer instead of creating a generic utility layer.

Architecture takeaways

  • Use a small composition chain when a sample has a linear workflow; separate metadata validation from expensive decoding state.
  • Give one object ownership of the mutable AVAssetReader pipeline so views issue intent rather than coordinate framework pieces.
  • Publish UI-consumed values on the main actor while keeping decode internals hidden.
  • An enum state makes asynchronous loading and failure paths explicit without several competing Boolean flags.
  • Protocol abstraction is unnecessary here because the app has one concrete decoding implementation and no test seam demonstrated in the source.

Source map

Source file Relevant symbols
StereoViewer/StereoViewerApp.swift StereoViewerApp
StereoViewer/LaunchView.swift LaunchView
StereoViewer/MediaDetailView.swift MediaDetailView
StereoViewer/MediaDetailViewModel.swift MediaDetailViewModel
StereoViewer/StereoView.swift StereoView
StereoViewer/StereoViewModel.swift StereoViewModelState, StereoViewModel, timestamp and layer-ID helpers