Drawing in the air and on surfaces with a spatial stylus
At a glance
| Item | Summary |
|---|---|
| Purpose | Create a spatial stylus drawing experience that balances latency and accuracy for both in-air and on-surface drawing. |
| App architecture | SurfaceInkingApp composes WindowGroup + ImmersiveSpace around StylusDrawingSettingsView; AppModel coordinates ARKit provider updates and maps anchors into RealityKit content. |
| Main patterns | SwiftUI scene composition, SwiftUI–RealityKit bridge, Explicit immersive-space lifecycle, Observable state owner, Provider session boundary, Async update consumer |
| Project style | Code-rich sample with 26 scanned Swift file(s) and 3127 Swift line(s); resources and generated assets are excluded from those counts. |
Project structure
Source bundle/
├── ARKit-SurfaceInking/ARKit-SurfaceInking/SurfaceInkingApp.swift # SurfaceInkingApp
├── ARKit-SurfaceInking/ARKit-SurfaceInking/Drawing/DrawingInputProvider.swift # StylusAnchorInput, DrawingInputProvider, TrackingMode
├── ARKit-SurfaceInking/ARKit-SurfaceInking/Models/AppModel.swift # AppModel, ImmersiveSpaceState, DrawingSettings
├── ARKit-SurfaceInking/ARKit-SurfaceInking/Models/StylusModel.swift # StylusButtonInput, Source, StylusModel
├── ARKit-SurfaceInking/ARKit-SurfaceInking/Views/StylusDrawingSettingsView.swift # StylusDrawingSettingsView, StylusSettingsHeaderView, StylusSettingsBodyView
├── ARKit-SurfaceInking/ARKit-SurfaceInking/Utilities/CurveProcessing.swift # HermiteInterpolant, HermiteControlPoint, SubdivisionSearchItem
├── ARKit-SurfaceInking/ARKit-SurfaceInking/Drawing/Mesh/BrushComponent.swift # BrushComponent, BrushSystem
├── ARKit-SurfaceInking/ARKit-SurfaceInking/Drawing/Style/BrushStyleProvider.swift # BrushStyleProvider, ThicknessType, Settings
└── ARKit-SurfaceInking/Packages/RealityKitContent/Package.realitycomposerpro/ProjectData/main.json # authored RealityKit content
Structure observations
- The runtime boundary is WindowGroup + ImmersiveSpace; the pruned tree lists only files that explain lifecycle, state, or framework integration.
- App code is split into role-named views, models, managers, providers, components, or systems.
- Authored
.realityor Reality Composer Pro content is a real implementation boundary; Swift loads or drives it rather than reproducing its entity graph.
Overall architecture
flowchart LR
SurfaceInkingApp_1["SurfaceInkingApp"]
WindowGroup___ImmersiveSpace_2["WindowGroup + ImmersiveSpace"]
StylusDrawingSettingsView_3["StylusDrawingSettingsView"]
AppModel_4["AppModel"]
RealityView_entity_graph_5["RealityView entity graph"]
ARKit_providers___RealityKit_6["ARKit providers + RealityKit"]
SurfaceInkingApp_1 --> WindowGroup___ImmersiveSpace_2
WindowGroup___ImmersiveSpace_2 --> StylusDrawingSettingsView_3
StylusDrawingSettingsView_3 --> AppModel_4
AppModel_4 --> RealityView_entity_graph_5
RealityView_entity_graph_5 --> ARKit_providers___RealityKit_6
Reference code
ARKit-SurfaceInking/ARKit-SurfaceInking/SurfaceInkingApp.swift:12 — the app or executable entry declares the outer scene lifecycle.
struct SurfaceInkingApp: App {
// ...
}The diagram is a responsibility flow, not a claim that every adjacent node directly calls the next. It keeps scene ownership, shared state, RealityKit content, and framework-provider work at separate levels.
Ownership and state
classDiagram
SurfaceInkingApp *-- AppModel : appModel
SurfaceInkingApp *-- StylusModel : stylusModel
StylusDrawingSettingsView o-- AppModel : appModel
StylusInputState --> AppModel : appModel
Ownership evidence
ARKit-SurfaceInking/ARKit-SurfaceInking/SurfaceInkingApp.swift:14 — representative stored state or the nearest verified lifecycle anchor.
@main
struct SurfaceInkingApp: App {
// ...
private let appModel = AppModel()
// ...
}| Owner | Object or state | Relationship | Mutation authority |
|---|---|---|---|
SurfaceInkingApp |
AppModel as appModel |
creates and retains | Only the declaring scope writes |
SurfaceInkingApp |
StylusModel as stylusModel |
creates and retains | Only the declaring scope writes |
StylusDrawingSettingsView |
AppModel as appModel |
receives a shared/non-owning reference | The upstream owner controls lifetime; this scope may invoke its mutable API |
StylusInputState |
AppModel as appModel |
stores and coordinates | Only the declaring scope writes |
Ownership here is deliberately narrow: @Environment and weak references are shared links, initialized @State or stored services are lifecycle ownership, and a RealityView content closure owns additions to its entity graph without making the SwiftUI view a reference-type owner.
Class and protocol design
| Type | Responsibility | Depends on or conforms to |
|---|---|---|
SurfaceInkingApp (ARKit-SurfaceInking/ARKit-SurfaceInking/SurfaceInkingApp.swift:12) |
Declares app scenes and top-level dependency lifetime. | App |
StylusDrawingSettingsView (ARKit-SurfaceInking/ARKit-SurfaceInking/Views/StylusDrawingSettingsView.swift:11) |
Presents UI and forwards gestures or lifecycle events. | View |
AppModel (ARKit-SurfaceInking/ARKit-SurfaceInking/Models/AppModel.swift:16) |
Owns observable feature state and domain transitions. | Concrete framework collaborators |
HermiteInterpolant (ARKit-SurfaceInking/ARKit-SurfaceInking/Utilities/CurveProcessing.swift:12) |
Declares a replaceable capability or lifecycle contract. | Concrete framework collaborators |
BrushSystem (ARKit-SurfaceInking/ARKit-SurfaceInking/Drawing/Mesh/BrushComponent.swift:15) |
Updates matching RealityKit entities. | System |
StylusModel (ARKit-SurfaceInking/ARKit-SurfaceInking/Models/StylusModel.swift:31) |
Owns observable feature state and domain transitions. | Concrete framework collaborators |
BrushStyleProvider (ARKit-SurfaceInking/ARKit-SurfaceInking/Drawing/Style/BrushStyleProvider.swift:16) |
Wraps a framework or transport capability. | Concrete framework collaborators |
Verified protocol-oriented seams: Float → HermiteInterpolant, SIMD2 → HermiteInterpolant, SIMD3 → HermiteInterpolant, SIMD4 → HermiteInterpolant. Framework conformances remain adapters, not proof of an app-wide protocol architecture.
Access control
| Symbol | Access | Verified effect | Likely rationale |
|---|---|---|---|
private(set) var curve: DrawingMeshGenerator (ARKit-SurfaceInking/ARKit-SurfaceInking/Drawing/CurveProcessing/SmoothCurveSampler.swift:31) |
private(set) |
The getter keeps its wider visibility, while mutation stays inside the declaring type and its permitted same-file extensions. | Inference: Protect a state invariant while allowing observation. |
private var brushState: BrushState (ARKit-SurfaceInking/ARKit-SurfaceInking/Drawing/DrawingDocument.swift:24) |
private |
Use is restricted to the declaration and same-file extensions permitted by Swift. | Inference: Hide implementation details and lifecycle-sensitive state. |
public let flatness: Float (ARKit-SurfaceInking/ARKit-SurfaceInking/Drawing/CurveProcessing/SmoothCurveSampler.swift:36) |
public |
The declaration is available to importing modules, subject to its containing type’s visibility. | Inference: Satisfy a cross-target or framework-facing surface without implying subclassability. |
No reviewed declaration uses fileprivate, open; unmodified Swift declarations are internal.
Reference code
ARKit-SurfaceInking/ARKit-SurfaceInking/Drawing/CurveProcessing/SmoothCurveSampler.swift:31 — representative visibility boundary.
private(set) var curve: DrawingMeshGenerator
/// A parameter that determines how closely the samples the smooth curve sampler generates should conform to the ideal smoothed curve.
///
/// Lower values of flatness results in more samples but a smoother curve.
public let flatness: FloatLogic ownership and placement
| Logic | Owning type or file | Placement rationale |
|---|---|---|
| Scene declaration and dependency lifetime | SurfaceInkingApp |
The App/entry boundary determines window, volume, and immersive-space lifetime. |
| Presentation, attachments, and gestures | StylusDrawingSettingsView |
SwiftUI view code forwards user intent and RealityView lifecycle events. |
| Shared feature state and commands | AppModel |
A role-named owner prevents sibling views from duplicating transitions. |
| Tracking authorization and update streams | ARKit-SurfaceInking/ARKit-SurfaceInking/Models/StylusModel.swift:37 |
Provider lifetime and async updates remain outside render-only view code. |
Design patterns
| Pattern | Source evidence | Purpose or tradeoff |
|---|---|---|
| SwiftUI scene composition | ARKit-SurfaceInking/ARKit-SurfaceInking/SurfaceInkingApp.swift:12 |
Keeps windows, volumes, and immersive-space lifecycle visible at the app boundary. |
| SwiftUI–RealityKit bridge | ARKit-SurfaceInking/ARKit-SurfaceInking/Views/ImmersiveDrawingView.swift:20 |
Builds and updates a RealityKit entity graph from SwiftUI lifecycle closures. |
| Explicit immersive-space lifecycle | ARKit-SurfaceInking/ARKit-SurfaceInking/SurfaceInkingApp.swift:31 |
Makes immersive presentation a scene transition rather than hidden global state. |
| Observable state owner | ARKit-SurfaceInking/ARKit-SurfaceInking/Models/AppModel.swift:16 |
Shares feature state across multiple views without moving framework resources into view values. |
| Provider session boundary | ARKit-SurfaceInking/ARKit-SurfaceInking/Models/StylusModel.swift:37 |
Owns provider lifetime separately from the SwiftUI view tree. |
| Async update consumer | ARKit-SurfaceInking/ARKit-SurfaceInking/Models/StylusModel.swift:53 |
Consumes provider or event streams in cancellable structured tasks. |
Naming conventions
- Role suffixes are evidence, not decoration: App:
SurfaceInkingApp; Model:AppModel,StylusModel; Provider:BrushStyleProvider,DrawingInputProvider; View:BrushStyleView,BrushTypeView,DeveloperDrawingMediumSettingsView,DeveloperSettingsView,ImmersiveDrawingView. - ECS names pair data and behavior: components none; systems
BrushSystem. - Protocols:
HermiteInterpolant. - Commands use verb-led methods:
isButtonPressed,getButtonPressure,getDrawingMedium,getChangedButtonInputIndex,updateButtonInputs,pruneButtonInputs,getCurrentDrawingMedium,getAnchorInput. - Files generally match their primary type;
Views,Models,Managers,Providers,Components,Systems, andPackagesfolders describe architectural roles where present.
Architecture takeaways
- Treat
SurfaceInkingAppas the owner of scene declarations, not as the owner of every RealityKit entity created later. - Keep view-local interaction in SwiftUI, but move provider sessions, playback resources, shared game state, or transport state into a stable owner when their lifetime exceeds one render pass.
- Run ARKit providers for the scene lifetime and consume their asynchronous updates in cancellable tasks; map anchors to entities at the boundary.
- Model immersive-space open, transition, and close states explicitly so windows and immersive content cannot drift apart.
Source map
| Source file | Relevant symbols |
|---|---|
ARKit-SurfaceInking/ARKit-SurfaceInking/SurfaceInkingApp.swift:12 |
SurfaceInkingApp |
ARKit-SurfaceInking/ARKit-SurfaceInking/Drawing/DrawingInputProvider.swift:24 |
StylusAnchorInput, DrawingInputProvider, TrackingMode, CorrectionMode, DrawingMedium, StylusInputState |
ARKit-SurfaceInking/ARKit-SurfaceInking/Models/AppModel.swift:16 |
AppModel, ImmersiveSpaceState, DrawingSettings, AnimationSettings |
ARKit-SurfaceInking/ARKit-SurfaceInking/Models/StylusModel.swift:15 |
StylusButtonInput, Source, StylusModel |
ARKit-SurfaceInking/ARKit-SurfaceInking/Views/StylusDrawingSettingsView.swift:11 |
StylusDrawingSettingsView, StylusSettingsHeaderView, StylusSettingsBodyView |
ARKit-SurfaceInking/ARKit-SurfaceInking/Utilities/CurveProcessing.swift:12 |
HermiteInterpolant, HermiteControlPoint, SubdivisionSearchItem |
ARKit-SurfaceInking/ARKit-SurfaceInking/Drawing/Mesh/BrushComponent.swift:10 |
BrushComponent, BrushSystem |
ARKit-SurfaceInking/ARKit-SurfaceInking/Drawing/Style/BrushStyleProvider.swift:16 |
BrushStyleProvider, ThicknessType, Settings |