// case study
Anchor
Accessible indoor navigation for iPhone: step-free routing, spoken landmark guidance, and LiDAR proximity alerts.
Anchor helps wheelchair users and blind and low-vision students get through campus buildings. It plans step-free routes, guides people landmark by landmark with speech, and has an experimental LiDAR mode that warns about nearby surfaces. Two rules shaped the whole design: never suggest a route the data can't support, and never invent directions.
- Context
- Team project, UB Forge
- Status
- In development, Oct 2026 – Present
- My role
- Designing step-free routing and spoken landmark guidance in SwiftUI; prototyping the LiDAR proximity alerts.
- Stack
- Swift
- SwiftUI
- ARKit
- RealityKit
- AVFoundation
- XCTest
- JavaScript
- Playwright
- GitHub Actions
// 01
Architecture
The app is split so the parts that have to be correct can be tested without a phone. Core contains the map model, validation, routing, step-by-step guidance, and depth math, using only Foundation: no UI, no ARKit. A small Swift package wraps Core so its tests run with swift test on macOS or Linux, no simulator required.
Services wrap platform APIs (settings in UserDefaults, speech, haptics), and Features holds the SwiftUI screens for Routes, Nearby, and Settings. Shared state lives in one observable app model.
demo_map.json
Loaded by CampusMap and checked by MapValidator before anything is routed.
RoutePlanner
Dijkstra on the directed graph, with the step-free preference from Settings.
Then one of:
Route
Summary, then RouteGuidance walks the user through each landmark, spoken by SpeechService or VoiceOver.
NoRouteDiagnosis
“No suitable route found,” plus the specific connections that block the trip.
// 02
Routing rules
Each bidirectional connection becomes two directed edges and a one-way connection becomes one, so an exit-only door is used leaving the building but never entering it.
- Step-free mode excludes anything not marked “confirmed” step-free. Stairs are excluded even if the data wrongly marks them confirmed, and the validator rejects that data anyway.
- Unavailable connections are always excluded. Unknown availability is allowed but shown as a “Before you go” warning.
- No silent fallback: if no route meets the preferences, the planner re-runs with blocked connections allowed but penalized, purely to explain which ones caused the failure. That relaxed route is never offered.
- Instructions are the authored text from the map. Lengths appear only when recorded, and no compass turns are generated.


// 03
Map data model
Every connection records physical suitability (step-free: confirmed, unsuitable, or unknown) separately from right-now availability (available, unavailable, unknown), plus a verification status, source, and who checked it and when. Display length and routing cost are separate fields, so elevator wait time can affect routing without ever being shown as a distance.
- A fictional demo map may only contain simulated connections.
- A verified connection must say when and by whom it was checked.
- Stairs can never be marked step-free.
// 04
LiDAR proximity alerts
Nearby reads ARKit's scene depth for the center of the camera view. Depth processing is pure math kept apart from ARKit, so it is unit-tested with hand-built arrays.
ARSession delegate
Background queue, up to 10 Hz. Copies the center 30% of each depth and confidence map.
DepthAnalyzer
Drops low-confidence and out-of-range pixels, then takes a 5th-percentile nearest distance.
NearbyViewModel
Main actor, ticks every 0.2 s and hides readings older than 0.6 s.
ProximityAlertPolicy
Decides when to fire haptics or speech, with hysteresis and rate limiting.
- Pixels below medium confidence or outside 0.1–5 m are dropped; if fewer than 25% remain, there is no reading rather than a guess.
- The nearest distance is the 5th percentile of usable depths, which ignores a few noisy pixels without missing a real surface.
- Alerts fire once on entering the threshold, repeat at most every 3 s, and re-arm only after the distance clears the threshold by 0.25 m, so a reading hovering at the edge can't flood the user.
- If reliable readings stop for 1 s, the user hears that depth readings are unavailable, so silence never means “all clear” by accident.
// 05
Accessibility
- All text uses system text styles, so Dynamic Type works; guidance buttons stack vertically at large sizes. Primary buttons are 44–56 pt tall.
- With VoiceOver on, each new step moves focus to the instruction so VoiceOver reads it, and the app doesn't also speak it, avoiding two voices at once.
- Without VoiceOver, AVSpeechSynthesizer speaks in navigation-prompt mode and ducks other audio.
- System colors only, so contrast follows iOS in light and dark mode.

// 06
Testing
- 40 XCTest unit tests cover the map model, validation, Dijkstra routing, guidance, depth math, and alert logic, including every demo scenario.
- A zero-dependency browser companion lets teammates on Windows and Linux review flows. It has 21 Node tests for routing and the local server, and 11 Playwright tests that include automated WCAG A/AA checks with axe in light and dark mode.
- GitHub Actions runs the web checks on every relevant push and pull request.
// 07
Status and limits
Anchor is at its first milestone, building toward a December 2026 demo at 43North. The map is fictional demo data, and every route screen says so. There is no indoor positioning yet (people confirm each landmark themselves) and no object recognition; Nearby measures distance to a surface, not what it is.


