Scanning objects using Object Capture
At a glance
| Item | Summary |
|---|---|
| Purpose | Implement a full scanning workflow for capturing objects on iOS devices. |
| App architecture | GuidedCaptureSampleApp injects the singleton AppDataModel; feature views drive onboarding and capture while the model owns ObjectCaptureSession state and CaptureFolderManager owns session-folder lifecycle. |
| Main patterns | SwiftUI scene composition, Observable state owner, Session owner boundary, Async update consumer, Protocol capability boundary, Capture-session state machine |
| Project style | Code-rich sample with 27 scanned Swift file(s) and 3961 implementation line(s). |
Project structure
Source bundle/
└── GuidedCaptureSample/
└── GuidedCaptureSample/
├── AppDataModel.swift # AppDataModel, ModelState, CaptureMode
├── CaptureFolderManager.swift # CaptureFolderManager, Error
├── Views/
│ ├── ContentView.swift # ContentView
│ ├── CaptureOverlayView.swift # CaptureOverlayView, TutorialView, LocalizedString
│ ├── ModelView.swift # ModelView, ARQuickLookController, Coordinator
│ └── PlayerView.swift # PlayerView, Coordinator, AVPlayerView
└── GuidedCaptureSampleApp.swift # GuidedCaptureSampleApp
Structure observations
- The runtime boundary is WindowGroup; the tree is pruned to lifecycle, state, framework, rendering, and ECS files.
- The source is Swift; role-named types and feature folders separate the major responsibilities.
Overall architecture
flowchart LR
GuidedCaptureSampleApp_1["GuidedCaptureSampleApp"]
WindowGroup_2["WindowGroup"]
ContentView_3["ContentView"]
AppDataModel_4["AppDataModel"]
Object_Capture_session_pipeline_5["Object Capture session pipeline"]
GuidedCaptureSampleApp_1 --> WindowGroup_2
WindowGroup_2 --> ContentView_3
ContentView_3 --> AppDataModel_4
AppDataModel_4 --> Object_Capture_session_pipeline_5
Reference code
GuidedCaptureSample/GuidedCaptureSample/GuidedCaptureSampleApp.swift:11 — executable or app-lifecycle anchor.
struct GuidedCaptureSampleApp: App {
static let subsystem: String = "com.example.apple-samplecode.guided-capture-sample"
var body: some Scene {
WindowGroup {
ContentView()
.environment(AppDataModel.instance)
}
}
}The diagram is a responsibility flow, not a claim that every adjacent node directly calls the next. It separates lifecycle, presentation, stable state, framework/session work, and the RealityKit entity graph.
Ownership and state
classDiagram
AppDataModel --> PhotogrammetrySession : photogrammetrySession
AppDataModel --> CaptureFolderManager : captureFolderManager
AppDataModel *-- AppDataModel : instance
AppDataModel --> CaptureMode : captureMode
Ownership evidence
GuidedCaptureSample/GuidedCaptureSample/AppDataModel.swift:35 — representative stored state or nearest verified lifecycle anchor.
private(set) var photogrammetrySession: PhotogrammetrySession?
/// When we start a new capture, the folder will be set here.
private(set) var captureFolderManager: CaptureFolderManager?
/// Shows whether the user decided to skip reconstruction.
private(set) var isSaveDraftEnabled = false| Owner | Object or state | Relationship | Mutation authority |
|---|---|---|---|
AppDataModel |
PhotogrammetrySession as photogrammetrySession |
stores and coordinates | The declaring type controls writes |
AppDataModel |
CaptureFolderManager as captureFolderManager |
stores and coordinates | The declaring type controls writes |
AppDataModel |
AppDataModel as instance |
creates and retains | The owning type coordinates writes |
AppDataModel |
CaptureMode as captureMode |
owns value state | The owning type coordinates writes |
Ownership is intentionally narrow: environment, bindings, observed objects, weak links, and delegate callbacks do not prove lifetime ownership. Initialized stored services and wrapper-managed state do; entities added inside a RealityKit content closure belong to that entity graph.
Class and protocol design
| Type | Responsibility | Depends on or conforms to |
|---|---|---|
GuidedCaptureSampleApp (GuidedCaptureSample/GuidedCaptureSample/GuidedCaptureSampleApp.swift:11) |
Declares app lifecycle and top-level dependency lifetime. | App |
ContentView (GuidedCaptureSample/GuidedCaptureSample/Views/ContentView.swift:14) |
Presents UI and forwards gestures or lifecycle events. | View |
AppDataModel (GuidedCaptureSample/GuidedCaptureSample/AppDataModel.swift:18) |
Owns feature state and domain transitions. | Identifiable |
OverlayButtons (GuidedCaptureSample/GuidedCaptureSample/Views/CaptureOverlayView.swift:237) |
Declares a replaceable capability or collaboration contract. | Concrete framework collaborators |
Coordinator (GuidedCaptureSample/GuidedCaptureSample/Views/ModelView.swift:40) |
Coordinates long-lived framework or cross-view work. | NSObject, QLPreviewControllerDataSource, QLPreviewControllerDelegate |
ARQuickLookController (GuidedCaptureSample/GuidedCaptureSample/Views/ModelView.swift:23) |
Coordinates long-lived framework or cross-view work. | UIViewControllerRepresentable |
CaptureFolderManager (GuidedCaptureSample/GuidedCaptureSample/CaptureFolderManager.swift:18) |
Coordinates long-lived framework or cross-view work. | Concrete framework collaborators |
AVPlayerView (GuidedCaptureSample/GuidedCaptureSample/Views/PlayerView.swift:99) |
Presents UI and forwards gestures or lifecycle events. | UIView |
Verified protocol-oriented seams: BottomOverlayButtons → OverlayButtons, TopOverlayButtons → OverlayButtons. Framework conformances remain adapters, not proof of an app-wide protocol architecture.
Access control
| Symbol | Access | Verified effect | Likely rationale |
|---|---|---|---|
private(set) var photogrammetrySession: PhotogrammetrySession? (GuidedCaptureSample/GuidedCaptureSample/AppDataModel.swift:35) |
private(set) |
The getter keeps its wider visibility, while writes stay in the declaring type and permitted same-file extensions. | Inference: Protect an invariant while allowing observation. |
private let logger = Logger(subsystem: GuidedCaptureSampleApp.subsystem, (GuidedCaptureSample/GuidedCaptureSample/AppDataModel.swift:13) |
private |
Use stays inside the declaration and same-file extensions permitted by Swift. | Inference: Hide lifecycle-sensitive state or an implementation step. |
No reviewed Swift access example uses fileprivate, public, open; declarations without a modifier are internal.
Reference code
GuidedCaptureSample/GuidedCaptureSample/AppDataModel.swift:35 — representative visibility boundary.
private(set) var photogrammetrySession: PhotogrammetrySession?
/// When we start a new capture, the folder will be set here.
private(set) var captureFolderManager: CaptureFolderManager?
/// Shows whether the user decided to skip reconstruction.
private(set) var isSaveDraftEnabled = false
var messageList = TimedMessageList()
enum ModelState {
// ...
}Logic ownership and placement
| Logic | Owning type or file | Placement rationale |
|---|---|---|
| Application/command lifecycle | GuidedCaptureSampleApp |
The executable boundary determines app, controller, scene, or command lifetime. |
| Presentation and user intent | ContentView |
The view/controller forwards input and owns only presentation-local work. |
| Shared feature state and commands | AppDataModel |
A stable owner prevents view recomputation or callbacks from duplicating transitions. |
| Capture/reconstruction session lifecycle | GuidedCaptureSample/GuidedCaptureSample/AppDataModel.swift:22 |
Session creation and output handling stay outside render-only view code. |
Design patterns
| Pattern | Source evidence | Purpose or tradeoff |
|---|---|---|
| SwiftUI scene composition | GuidedCaptureSample/GuidedCaptureSample/GuidedCaptureSampleApp.swift:11 |
Keeps windows, volumes, and immersive presentation visible at the app boundary. |
| Observable state owner | GuidedCaptureSample/GuidedCaptureSample/AppDataModel.swift:18 |
Keeps shared feature state in a stable owner rather than in transient view values. |
| Session owner boundary | GuidedCaptureSample/GuidedCaptureSample/AppDataModel.swift:22 |
Keeps authorization, session lifetime, and framework callbacks in a stable model, manager, or controller. |
| Async update consumer | GuidedCaptureSample/GuidedCaptureSample/AppDataModel.swift:163 |
Consumes long-lived framework output in a cancellable asynchronous task. |
| Protocol capability boundary | GuidedCaptureSample/GuidedCaptureSample/Views/BottomOverlayButtons.swift:14 |
Defines a local capability with concrete conformers; this is the verified protocol-oriented seam. |
| Capture-session state machine | GuidedCaptureSample/GuidedCaptureSample/AppDataModel.swift:22 |
Centralizes session lifecycle and exposes explicit capture states to the many guidance and control views. |
Naming conventions
- Role suffixes are evidence, not decoration: App:
GuidedCaptureSampleApp; Model:AppDataModel; Coordinator:Coordinator; Controller:ARQuickLookController; Manager:CaptureFolderManager. - Protocols:
OverlayButtons. - Commands use verb-led methods:
endCapture,removeCaptureFolder,setShowOverlaySheets,saveDraft,attachListeners,detachListeners,handleAppTermination,startNewCapture. - Files and folders use primary-type or responsibility names such as
Views,Models,Rendering,Components,Systems,Packages, or feature names where those boundaries exist.
Architecture takeaways
- Treat
GuidedCaptureSampleAppas the owner of executable or scene lifecycle, not automatically as the owner of every entity created later. - Keep view/controller input near presentation, but move sessions, reconstruction, capture, rendering, shared game state, or documents into stable owners when their lifetime exceeds one callback or render pass.
- Tie asynchronous session/output consumers to an explicit owner so cancellation and teardown follow the same lifecycle.
Source map
| Source file | Relevant symbols |
|---|---|
GuidedCaptureSample/GuidedCaptureSample/AppDataModel.swift:18 |
AppDataModel, ModelState, CaptureMode |
GuidedCaptureSample/GuidedCaptureSample/CaptureFolderManager.swift:18 |
CaptureFolderManager, Error |
GuidedCaptureSample/GuidedCaptureSample/Views/ContentView.swift:14 |
ContentView |
GuidedCaptureSample/GuidedCaptureSample/GuidedCaptureSampleApp.swift:11 |
GuidedCaptureSampleApp |
GuidedCaptureSample/GuidedCaptureSample/Views/CaptureOverlayView.swift:15 |
CaptureOverlayView, TutorialView, LocalizedString, BoundingBoxGuidanceView, OverlayButtons |
GuidedCaptureSample/GuidedCaptureSample/Views/ModelView.swift:14 |
ModelView, ARQuickLookController, Coordinator, QLPreviewControllerWrapper |
GuidedCaptureSample/GuidedCaptureSample/Views/PlayerView.swift:11 |
PlayerView, Coordinator, AVPlayerView |