Capturing depth using the LiDAR camera
At a glance
| Item | Summary |
|---|---|
| Purpose | Stream or photograph synchronized LiDAR depth and color data, then present several Metal visualizations. |
| App architecture | SwiftUI owns CameraManager; the manager owns a capture controller and shared captured-data object; delegate callbacks update views backed by reusable Metal coordinators. |
| Main patterns | View-model adapter, weak delegate, synchronized outputs, protocol/generic rendering layer, shared Metal environment. |
| Project style | Model/ contains capture/data coordination; Views/Metal/ contains protocol-based render wrappers, coordinators, and shaders. |
Project structure
LiDARDepth/
├── LiDARDepthApp.swift
├── ContentView.swift
├── Model/
│ ├── CameraManager.swift
│ ├── CameraController.swift
│ └── CVPixelBuffer+Extension.swift
└── Views/
├── DepthOverlay.swift
├── ZoomOnTap.swift
├── View+Extensions.swift
└── Metal/
├── MetalContentView.swift
├── MetalEnvironment.swift
├── MetalPointCloud.swift
├── MetalTexture*.swift
└── Shaders/shaders.metal
Structure observations
- The model layer separates UI-facing state (
CameraManager) from AVFoundation session ownership (CameraController). - Metal view wrappers and renderer coordinators are grouped by implementation technology.
- All visualizations share
CameraCapturedDataby reference and a singleton Metal device/queue/library.
Overall architecture
flowchart LR
App["LiDARDepthApp"] --> View["ContentView"]
View --> Manager["CameraManager"]
Manager --> Controller["CameraController"]
Controller --> Session["AVCaptureSession"]
Session --> Sync["Video + depth synchronizer"]
Session --> Photo["Photo output"]
Sync -->|CaptureDataReceiver| Manager
Photo -->|CaptureDataReceiver| Manager
Manager --> Data["CameraCapturedData"]
Data --> MetalViews["MetalRepresentable views"]
MetalViews --> Coordinators["MTKCoordinator subclasses"]
Coordinators --> Environment["MetalEnvironment.shared"]
Reference code
LiDARDepth/ContentView.swift:12 and LiDARDepth/Model/CameraManager.swift:31 — UI-to-capture composition
struct ContentView: View {
@StateObject private var manager = CameraManager()
}
init() {
capturedData = CameraCapturedData()
controller = CameraController()
controller.startStream()
controller.delegate = self
}Interpretation
SwiftUI retains one observable manager. The manager adapts delegate deliveries to published UI state and mutates a stable captured-data reference consumed by renderers. The controller remains the sole capture-graph implementation, while a protocol/generic coordinator layer shares Metal view setup.
Ownership and state
classDiagram
ContentView *-- CameraManager : StateObject creates
CameraManager *-- CameraController : creates
CameraManager *-- CameraCapturedData : creates and mutates
CameraController *-- AVCaptureSession : creates
CameraController *-- AVCaptureVideoDataOutput : creates
CameraController *-- AVCaptureDepthDataOutput : creates
CameraController *-- AVCapturePhotoOutput : creates
CameraController o-- CaptureDataReceiver : weak delegate
MetalEnvironment *-- MTLDevice : singleton owns
MetalEnvironment *-- MTLCommandQueue : singleton owns
Ownership evidence
LiDARDepth/Model/CameraManager.swift:27 — manager composition
let controller: CameraController
init() {
capturedData = CameraCapturedData()
controller = CameraController()
controller.isFilteringEnabled = true
controller.startStream()
controller.delegate = self
}| Owner | Object or state | Relationship | Mutation authority |
|---|---|---|---|
ContentView |
CameraManager |
Creates/retains with @StateObject |
Manager publishes state to SwiftUI. |
CameraManager |
Controller, captured data, Combine cancellables | Creates and retains all | Manager updates published state and captured textures. |
CameraController |
Session, outputs, synchronizer, texture cache | Creates during initialization | Controller and delegate callbacks. |
CameraController |
CaptureDataReceiver |
Weak reference supplied by manager | Receiver handles delivered value; controller does not own manager. |
MetalEnvironment |
Device, queue, library | Process-wide singleton owns immutable Metal services | Private initializer; shared instance only. |
Class and protocol design
classDiagram
class CaptureDataReceiver {
<<protocol>>
onNewData()
onNewPhotoData()
}
class MetalRepresentable {
<<protocol>>
rotationAngle
}
CameraManager ..|> CaptureDataReceiver
CameraController --> CaptureDataReceiver : weak delegate
MetalPointCloudView ..|> MetalRepresentable
MetalTextureView ..|> MetalRepresentable
MTKCoordinator --> MetalRepresentable : generic parent
MTKPointCloudCoordinator --|> MTKCoordinator
Reference code
LiDARDepth/Model/CameraController.swift:12 and LiDARDepth/Views/Metal/MetalContentView.swift:18 — capture and rendering capabilities
protocol CaptureDataReceiver: AnyObject {
func onNewData(capturedData: CameraCapturedData)
func onNewPhotoData(capturedData: CameraCapturedData)
}
protocol MetalRepresentable: UIViewRepresentable {
var rotationAngle: Double { get set }
}| Type | Responsibility | Depends on |
|---|---|---|
CameraManager |
Bridge capture events and controls to observable UI state | CameraController, Combine, CameraCapturedData |
CameraController |
Configure LiDAR formats/outputs and deliver synchronized/photo data | AVFoundation, Core Video, delegate protocol |
CaptureDataReceiver |
Decouple controller delivery from UI model implementation | CameraCapturedData |
MetalRepresentable |
Share SwiftUI-to-MTKView setup contract |
SwiftUI, MetalKit |
MTKCoordinator<Parent> |
Base MTKViewDelegate and reusable pipeline setup |
MetalRepresentable, MetalEnvironment |
MetalEnvironment |
Share immutable Metal device/queue/library | Metal |
Access control
| Symbol | Access | Verified effect | Likely rationale |
|---|---|---|---|
ContentView.manager |
private |
Only the root view accesses its manager instance | Makes the screen the clear lifecycle owner. |
| Controller outputs, synchronizer, queue, cache, and setup methods | private |
Capture graph cannot be rewired by manager/views | Preserves controller session invariants. |
CameraController.captureSession |
private(set) |
Manager may read/use the session but cannot replace it | Exposes observation/preview access without giving away construction authority. |
CameraController.delegate |
weak, implicit internal |
Avoids controller-manager retain cycle | Standard callback ownership. |
MetalEnvironment.init |
private |
Only shared can construct the environment |
Enforces one shared Metal resource set. |
| App types/protocols | implicit internal |
Visible only inside the app module | No public framework API is intended. |
Reference code
LiDARDepth/Model/CameraController.swift:24 — protected capture graph
private let videoQueue = DispatchQueue(
label: "com.example.apple-samplecode.VideoQueue",
qos: .userInteractive
)
private(set) var captureSession: AVCaptureSession!
private var photoOutput: AVCapturePhotoOutput!
private var depthDataOutput: AVCaptureDepthDataOutput!
weak var delegate: CaptureDataReceiver?No public, open, or fileprivate declaration appears in the Swift source.
Logic ownership and placement
| Logic | Owning type or file | Placement rationale |
|---|---|---|
| SwiftUI controls and visualization grid | ContentView |
Presentation and local depth-range state. |
| Observable capture mode/filter/orientation state | CameraManager |
UI-facing adapter and delegate receiver. |
| LiDAR device/format/output setup | CameraController private methods |
Concrete owner of capture session and hardware requirements. |
| Synchronizing/video-depth packaging | Controller synchronizer delegate extension | Runs on delivery queue with access to outputs/cache. |
| Photo-depth packaging | Controller photo delegate extension | Handles the distinct one-shot capture lifecycle. |
| Reusable Metal view setup | MetalContentView.swift |
Shared protocol/default implementation/generic coordinator. |
| Renderer-specific draw code | Individual MetalTexture.../MetalPointCloud files |
One visualization algorithm per type/file. |
Design patterns
| Pattern | Source evidence | Purpose or tradeoff |
|---|---|---|
| View-model adapter | LiDARDepth/Model/CameraManager.swift:14 |
Converts capture callbacks into published SwiftUI state and commands. |
| Weak delegate | LiDARDepth/Model/CameraController.swift:12 and line 37 |
Decouples delivery and avoids ownership cycle. |
| Data-output synchronizer | LiDARDepth/Model/CameraController.swift:128 |
Delivers matching video/depth data as one unit. |
| Protocol with constrained default implementation | LiDARDepth/Views/Metal/MetalContentView.swift:18 and line 23 |
Reuses MTKView creation/update behavior across visualization wrappers. |
| Generic coordinator base class | LiDARDepth/Views/Metal/MetalContentView.swift:46 |
Shares Metal view setup and lets subclasses override pipeline/draw stages. |
| Singleton environment | LiDARDepth/Views/Metal/MetalEnvironment.swift:13 |
Shares one Metal device, command queue, and library; introduces global lifetime. |
Main application flow
sequenceDiagram
participant Session as Capture session
participant Sync as Data synchronizer
participant Controller as CameraController
participant Manager as CameraManager
participant View as Metal views
Session->>Sync: video + depth outputs
Sync->>Controller: synchronized collection
Controller->>Controller: create textures and CameraCapturedData
Controller->>Manager: onNewData(capturedData)
Manager->>Manager: update stable capturedData reference
Manager-->>View: published dataAvailable/state
View->>View: coordinators render textures
Reference code
LiDARDepth/Model/CameraController.swift:159 — synchronized delivery
func dataOutputSynchronizer(_ synchronizer: AVCaptureDataOutputSynchronizer,
didOutput collection: AVCaptureSynchronizedDataCollection) {
let syncedDepthData = collection.synchronizedData(for: depthDataOutput)
let syncedVideoData = collection.synchronizedData(for: videoDataOutput)
// Package both streams as CameraCapturedData.
delegate?.onNewData(capturedData: data)
}Naming conventions
- Types: layer roles are explicit (
CameraManager,CameraController,CameraCapturedData,MetalEnvironment). - Protocols: receiver/capability names (
CaptureDataReceiver,MetalRepresentable). - Methods: event callbacks use
onNew...; commands usestart,resume,capture, andsetupverbs. - Views/coordinators: paired
Metal...ViewandMTK...Coordinatornames identify SwiftUI wrapper versus renderer delegate. - Files:
Model/versusViews/Metal/organization mirrors capture/state and rendering responsibilities.
Architecture takeaways
- Separate observable UI coordination from the object that owns AVFoundation hardware/session details.
- A weak receiver protocol keeps capture delivery reusable without making the controller own the UI layer.
- Store a stable reference object when many renderer views consume textures that update frame by frame.
- Protocol default implementations and a generic coordinator remove repeated SwiftUI/MetalKit bridge setup.
private(set)and a private singleton initializer clearly encode who may construct or replace critical resources.
Source map
| Source file | Relevant symbols |
|---|---|
LiDARDepth/LiDARDepthApp.swift |
App entry |
LiDARDepth/ContentView.swift |
Manager ownership and visualization composition |
LiDARDepth/Model/CameraManager.swift |
UI adapter, delegate receiver, captured data |
LiDARDepth/Model/CameraController.swift |
Capture graph, synchronizer/photo delegates |
LiDARDepth/Views/Metal/MetalContentView.swift |
MetalRepresentable, generic coordinator |
LiDARDepth/Views/Metal/MetalEnvironment.swift |
Shared Metal resources |
LiDARDepth/Views/Metal/MetalPointCloud.swift |
Example renderer specialization |