Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ARDemo

Platform Swift ARKit RealityKit

A portfolio-grade iOS AR demo showcasing clean MVVM architecture, protocol-driven dependency injection, and a unified abstraction for placing both .usdz and .reality 3D models into the real world via ARKit and RealityKit.


Overview

ARDemo lets you pick from three bundled 3D models and place them on real-world surfaces detected by ARKit. Once placed, you can move, rotate, and scale each model using touch gestures. The codebase is written to demonstrate architectural decisions as clearly as the AR features themselves — it is a showcase of how to build an iOS AR app, not just a proof that AR works.


Architecture

┌───────────────────────────────────────────────────────┐
│                      SwiftUI Views                    │
│  ModelSelectionView  ─── ARScreen ─── AROverlayView  │
└──────────────┬──────────────────────────┬────────────┘
               │ @ObservedObject          │ @ObservedObject
┌──────────────▼──────────┐  ┌───────────▼────────────┐
│ ModelSelectionViewModel │  │      ARViewModel        │
│  • models: [ARModel]    │  │  • sessionState         │
│  • selectedModel        │  │  • placementState       │
└─────────────────────────┘  │  • isModelPlaced        │
                             └─────┬─────┬───────┬─────┘
                                   │     │       │
                          protocol │     │proto  │ protocol
                    ┌──────────────▼─┐ ┌─▼──────▼────┐
                    │ ARSessionManag-│ │ ModelLoadable│
                    │ ing            │ │              │
                    └───────┬────────┘ └──────┬───────┘
                            │                 │
                    ┌───────▼────────┐ ┌──────▼───────┐
                    │ARSessionManager│ │ ModelLoader  │
                    │ (ARSession)    │ │ .usdz/.reali-│
                    └───────┬────────┘ │ ty dispatch  │
                            │          └──────────────┘
                    ┌───────▼────────┐
                    │  SceneManager  │
                    │ (AnchorEntity) │
                    └───────┬────────┘
                            │
                    ┌───────▼────────┐
                    │ RealityKit     │
                    │ ARView +       │
                    │ installGesture │
                    └────────────────┘

Layer responsibilities

Layer Files Role
Models ARModel, ARPlacementState, ARSessionState Pure value types — no imports beyond Foundation
Scene ModelLoader, SceneManager, ModelEntity+Extensions RealityKit manipulation; all @MainActor
ARSession ARSessionManager, PlaneDetector ARKit session lifecycle; exposes Combine publishers
ViewModels ARViewModel, ModelSelectionViewModel State machines; depend only on protocols
Views ARViewContainer, AROverlayView, ModelSelectionView, Components SwiftUI + UIViewRepresentable
App AppCoordinator, ARDemoApp Composition root — wires the dependency graph

Tech Stack

Technology Usage
ARKit World tracking, plane detection (horizontal + vertical)
RealityKit Entity rendering, AnchorEntity, installGestures
SwiftUI All UI — NavigationStack, LazyVGrid, overlays
Combine Session and plane-detection state publishers
Swift Concurrency async/await entity loading; @MainActor isolation

Supported 3D Formats

Format Extension Loading API
Universal Scene Description (zip) .usdz ModelEntity(named:in:) async initialiser
Reality Composer scene .reality Entity(contentsOf:withName:) async initialiser

ModelLoader presents a single load(model:) async throws -> Entity API regardless of format, selected by a switch on ARModelFormat. ViewModels and scene managers never see the file format — they work with Entity objects only.


Bundled Models

Model Format Anchor / Scene
Lunar Rover .reality Scene name "Rover Flow"
Toy Biplane .usdz N/A
Toy Car .usdz N/A
Lunar Habitat .reality Scene name "Flow"

Features

  • Plane detection — scans for horizontal and vertical surfaces before allowing placement
  • Model selection — 2-column grid with format badges (USDZ / REALITY)
  • Preview placement — model appears at tap location before confirmation
  • Confirm / Cancel — explicit confirmation step prevents accidental placements
  • Drag to move — slide placed model along detected planes
  • Rotate — twist gesture rotates around Y axis
  • Pinch to scale — uniform scale clamped between 0.1× and 3.0×
  • Reset — clears the scene and returns to plane scanning
  • Session lifecycle — pauses on app background, resumes on foreground
  • Error handling — typed ModelLoadError surfaced in the overlay

Project Structure

ARDemo/
├── App/
│   ├── ARDemoApp.swift            ← @main entry point
│   └── AppCoordinator.swift       ← composition root / navigation
├── ARSession/
│   ├── ARSessionManager.swift     ← owns ARSession lifecycle; Combine publisher
│   └── PlaneDetector.swift        ← tracks ARPlaneAnchor additions/removals
├── Scene/
│   ├── ModelLoader.swift          ← unified async API for .usdz and .reality
│   ├── ModelEntity+Extensions.swift  ← uniformScale, rotateAroundY, ScaleConstraints
│   └── SceneManager.swift         ← places/removes AnchorEntity in the scene
├── ViewModels/
│   ├── ModelSelectionViewModel.swift
│   └── ARViewModel.swift
├── Views/
│   ├── ModelSelectionView.swift
│   ├── ARViewContainer.swift      ← UIViewRepresentable + ARScreen
│   ├── AROverlayView.swift
│   └── Components/
│       ├── ModelThumbnailView.swift
│       └── PlacementConfirmationView.swift
├── Models/
│   ├── ARModel.swift              ← ARModel struct + static catalog
│   ├── ARPlacementState.swift
│   └── ARSessionState.swift
└── Resources/
    ├── LunarRover_English.reality
    ├── toy_biplane_realistic.usdz
    └── toy_car.usdz

ARDemoTests/
├── ARModelTests.swift
├── ModelLoaderTests.swift
├── ARViewModelTests.swift
├── SceneManagerTests.swift
└── Mocks/
    ├── MockModelLoader.swift
    ├── MockARSessionManager.swift
    └── MockSceneManager.swift

How to Run

Note: ARKit requires a physical device. The iOS Simulator does not support camera access or ARKit. You need an iPhone (A12 Bionic or later) to run this app.

  1. Clone the repository
  2. Open ARDemo.xcodeproj in Xcode 15 or later
  3. Select your physical iPhone as the run destination
  4. Build and run (⌘R)
  5. Grant camera permission when prompted
  6. Point the camera at a flat surface and move slowly to detect planes

Requirements

Requirement Minimum
iOS 18.0
Device iPhone with A12 Bionic or later
Xcode 15.0+
Swift 5.9+

Architecture Decisions

MVVM with protocol-driven dependencies

All inter-layer boundaries use protocols (ModelLoadable, ARSessionManaging, SceneManaging). No ViewModel instantiates its own collaborators — they are injected by AppCoordinator. This makes every ViewModel unit-testable without a real device, real camera, or real RealityKit session.

Why ModelLoader exists

.usdz files use ModelEntity(named:in:) — a high-level async initialiser that handles mesh decompression. .reality files require Entity(contentsOf:withName:) followed by a findEntity(named:) tree walk to locate the named scene. These are fundamentally different loading paths. ModelLoader contains the only switch on ARModelFormat in the entire codebase: all other code receives an Entity and is completely format-agnostic.

@MainActor throughout RealityKit

In iOS 17+ SDKs, all Entity loading and property access is @MainActor-isolated. Protocols that involve RealityKit types (ModelLoadable, SceneManaging) are therefore declared @MainActor so the compiler enforces isolation at the protocol level rather than at each call site.

ARViewContainer (not ARView)

The UIViewRepresentable wrapper is named ARViewContainer to avoid shadowing RealityKit.ARView. Without this distinction, every file that uses the type-level name ARView would need a module qualifier (RealityKit.ARView), which is noisy and error-prone.

PlaneDetector as a separate type

ARSessionManager could expose plane detection directly, but separating it into PlaneDetector keeps single-responsibility clean: ARSessionManager owns the session lifecycle; PlaneDetector owns the set of known planes and the hasDetectedPlane publisher. ARViewModel subscribes to PlaneDetector.hasDetectedPlanePublisher independently.


Testing

Tests live in ARDemoTests/ and use XCTest:

Test class What it tests
ARModelTests Catalog count, correct formats, ID uniqueness, Equatable semantics
ModelLoaderTests Success path, fileNotFound, loadFailed, anchorNotFound error descriptions
ARViewModelTests Initial state, startSession, plane-detection state transitions, resetScene, pauseSession
SceneManagerTests placeEntity sets currentPlacedEntity, removeAllEntities clears it

All tests run against mock collaborators — no device, no ARKit, no RealityKit engine required. The mocks are in ARDemoTests/Mocks/ and conform to the same protocols as the production implementations.


Author

Ayush Kumar Sethi
aks5686.github.io · github.com/aks5686

About

Production-grade ARKit demo showcasing 3D object placement, gesture interaction, and clean MVVM architecture on iOS.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages