GT iOS Club Fall 2026 - AccessAble app
AccessAble/ Xcode project folder
├── AccessAble.xcodeproj open this
├── Info.plist usage strings + background modes (merged with the generated plist)
└── AccessAble/ all app source (synced folder: new files are added automatically)
├── AccessAbleApp.swift @main entry point
├── PrivacyInfo.xcprivacy privacy manifest
├── Assets.xcassets
├── Models/ plain data + services. No SwiftUI imports.
│ ├── Core/ Phase 0 shared contracts. Changes need a review from all three subteams.
│ │ ├── AnnouncementEvent.swift category, priority, text, HapticID, bearing, source, expiresAt, dedupKey
│ │ ├── GuidanceProfile.swift vision level, channels, verbosity, speech rate, haptic intensity, categories
│ │ ├── HapticID.swift haptic vocabulary v1 (one AHAP file per case)
│ │ ├── TripState.swift trip phase, next maneuver, distances, GeoCoordinate
│ │ ├── SceneObservation.swift detections, ground surface, recognized text, elevation hazards
│ │ ├── ElevationHazard.swift stepUp / stepDown / dropOff / unknown
│ │ ├── PerformanceMode.swift normal / battery
│ │ └── RelativeBearing.swift direction relative to the user's facing
│ ├── Services/
│ │ ├── ServiceProtocols.swift the interfaces between subteams
│ │ ├── AppServices.swift dependency container; swap placeholders for real services here
│ │ ├── PlaceholderServices.swift stand-ins so the app builds and previews on day one
│ │ ├── PerformanceModeMonitor.swift Low Power Mode + thermal → PerformanceMode
│ │ ├── Broadcaster.swift one-to-many AsyncStream helper (latest value by default)
│ │ └── Vision/ CV runtime: ARKit, Vision and Core ML (needs a device)
│ │ ├── ARSessionManager.swift the app's only ARSession (T1, T2)
│ │ ├── ARSceneAnalyzer.swift services.scene: frames → perception → observations + announcements
│ │ ├── ScenePerception.swift one frame → SceneObservation + obstacle announcements
│ │ ├── TextReader.swift OCR at ≤1 Hz beside the safety loop (T17)
│ │ └── CameraImageOrientation.swift interface orientation → image orientation (T4)
│ ├── Formatting/
│ │ ├── SpokenPhrasing.swift shared speech rules ("about 2 metres", "on your left")
│ │ └── SceneDescriptionBuilder.swift "Describe my surroundings", offline (T18)
│ └── Vision/ CV algorithms; no ARKit or UIKit, tested with `swift test`
│ ├── LatestOnlyProcessor.swift newest-frame-only processing queue (T3)
│ ├── DepthMap.swift filtered LiDAR depth, one copy per frame (T6)
│ ├── DepthCoordinateMapping.swift upright boxes → depth-map coordinates (T5)
│ ├── DetectionDistanceEstimator.swift robust distance inside a box (T7)
│ ├── ObjectLocation.swift box + depth → world position, distance, bearing
│ ├── DepthPointCloud.swift depth map → world point cloud
│ ├── FloorPlaneDetector.swift RANSAC floor fit (T10)
│ ├── WalkingCorridor.swift the strip of space ahead of the user (T9)
│ ├── GeometricObstacleDetector.swift obstacles by geometry, no labels (T11)
│ ├── GroundProfiler.swift, ElevationHazardDetector.swift kerbs, steps, drop-offs (T12)
│ ├── PeopleDetector.swift, PeopleDetection.swift Vision people detection (T14)
│ ├── ObjectDetector.swift Core ML object detection, YOLO11n (T13)
│ ├── DetectionFusion.swift labels geometric obstacles with recognized objects
│ ├── ObjectTracker.swift stable track IDs across frames (T15)
│ ├── ObstacleAnnouncer.swift when to announce what (T16)
│ ├── TextDetector.swift, RecognizedTextMemory.swift OCR and de-duplication (T17)
│ ├── FrameHealthCheck.swift session acceptance checks (T1)
│ └── Evaluation/ precision/recall, distance error, FPS/latency, test set (T8, T20)
├── ViewModels/ @MainActor @Observable classes. No SwiftUI imports.
│ ├── AppViewModel.swift app-wide state (profile, onboarding, services)
│ ├── HomeViewModel.swift reference example of the view model pattern
│ ├── Vision/ Subteam 1 — Computer Vision
│ │ ├── LiveSceneViewModel.swift debug view of the shared services.scene pipeline
│ │ └── PhotoDetectionViewModel.swift T14 against a still image (works in the Simulator)
│ ├── Maps/ Subteam 2 — Maps + Location
│ └── Interaction/ Subteam 3 — User Interaction
└── Views/ SwiftUI only. No business logic.
├── RootView.swift, HomeView.swift
├── Components/ shared accessible building blocks
└── Vision/ Maps/ Interaction/
Maps geometry reference (distances, bearings, route projection): Docs/Maps/Geometry.md.
Maps navigation reference (trip tracking, cues, rerouting, known limits): Docs/Maps/Navigation.md.
- Models are value types (
struct/enum),Sendable, andCodablewhere they get persisted. Services (camera, location, announcement engine) also live underModels/Services, behind a protocol. - ViewModels are
@MainActor @Observable final class. They takeAppServices(orAppViewModel) ininitand depend only on protocols. Long-running subscriptions go infunc start() async. - Views own their view model with
@State, call.task { await viewModel.start() }, and forward actions to the view model. SeeHomeView+HomeViewModelfor the reference example. - Cross-team communication only goes through
ServiceProtocols.swift. CV and Maps never speak or buzz directly; they submit anAnnouncementEventtoservices.announcements, and the engine (3.3) decides what to say. - Put your subteam's files in your subteam's subfolder (
Vision/,Maps/,Interaction/) inside each of the three layers. That keeps merge conflicts between branches low. - When your real service is ready, replace the matching
Placeholder…inAppServices.live. Keep the placeholder for previews and tests.
Models/Vision/ holds pure algorithms, so they are tested on the Mac rather than in a
simulator. Keep ARKit and UIKit out of it: anything that needs them goes in
Models/Services/Vision/, which the test harness does not compile. Package.swift at the repo root is a test-only SwiftPM harness that compiles
the same source files the app target does — it is not a dependency of the app, and the
Xcode project does not know it exists.
swift test --scratch-path /tmp/accessable-buildThe --scratch-path is not optional here. This repo sits under ~/Documents, which
iCloud Drive syncs, and the sync daemon stamps com.apple.FinderInfo onto the freshly
built .xctest bundle — which makes codesign fail with "resource fork, Finder
information, or similar detritus not allowed". Building outside the synced tree sidesteps
it. A plain swift test works if you move the repo somewhere iCloud does not touch.
It lives at the repo root because SwiftPM refuses target paths outside the package root.
When you add a file to Models/Vision/, the Xcode target picks it up through the
synchronized folder group automatically; the package picks it up too, since it compiles the
whole Vision directory.
Home → Camera guidance runs the real pipeline and announces obstacles through the
announcement engine. Home → Live detection opens LiveSceneView, a debug view of the
same pipeline: the ARKit camera feed with boxes drawn over it (green = recognized objects
and people, orange = geometric obstacles, relabelled when a model recognizes them), the
fitted floor, any kerbs or drop-offs ahead, measured analysis FPS and latency, and the
ARKit tracking state.
Both need a physical device — ARKit world tracking does not run in the Simulator — and
a LiDAR device (iPhone 12 Pro or newer) for distances, the floor, obstacles and kerbs.
In the Simulator AppServices.live falls back to PlaceholderSceneProvider, and Live
detection offers Detect people in a photo instead, which runs the real Vision request
over a photo from the library.
Without LiDAR you still get recognized objects and people, hasMetricDepth stays false,
and nothing distance-based is announced.
See docs/ComputerVision.md for the CV architecture, data contracts and task status.
- Every control has a label, a hint where the action isn't obvious, and correct traits (
.accessibleControl(...)). - Hit targets are at least 44×44 pt (
.minimumTapTarget()). - Text uses Dynamic Type styles and works at accessibility sizes. No fixed font sizes.
- Icons that duplicate text are
.accessibilityHidden(true). - Test the screen with VoiceOver on (and Screen Curtain) before opening a PR.
Open AccessAble/AccessAble.xcodeproj. Settings the code depends on (already set in the project, don't change them):
- iOS Deployment Target 26.2, Swift Language Version 6.
- Default Actor Isolation: nonisolated (the Xcode template default is MainActor). View models are marked
@MainActorexplicitly; models and services must stay off the main actor so camera/LiDAR processing doesn't block the UI. INFOPLIST_FILE = Info.plistwithGENERATE_INFOPLIST_FILE = YES. Add new usage strings toAccessAble/Info.plist. Keep it outside the innerAccessAble/source folder, or Xcode will also copy it as a resource and the build fails.- Only files that belong in the app go inside the inner
AccessAble/folder — anything there (including docs) is bundled.