Free iOS app (iPhone-only): match a photo to a geolocated historical photograph from the same location. Core interaction is a draggable vertical slider revealing the historical image beneath the present-day photo. All matching happens on-device against a bundled SQLite index; no backend, no accounts.
- Language: Swift 5.10+
- UI: SwiftUI (iOS 17+ minimum — no UIKit views except AVFoundation camera wrapper)
- Database: SQLite via GRDB.swift 7.x — typed Swift wrappers, fast spatial queries
- Image loading: Kingfisher 8.x — async fetch + disk cache for thumbnails
- Image ML: Vision framework (
VNGenerateImageFeaturePrintRequest) — on-device feature print similarity - Location: CoreLocation (CLLocationManager + CLHeading)
- Camera: AVFoundation (photo capture pipeline)
- Data pipeline: Python 3.12 + aiohttp + sqlite3 (dev-time only, not shipped)
Requires Xcode (verified against 26.6). There is no Package.swift: this is an Xcode project,
so everything runs through xcodebuild against a simulator. The Makefile wraps that, and picks
the first available iPhone simulator exactly the way .github/workflows/ci.yml does.
make build # compile for the simulator
make test # full suite: 55 tests, ~3min. 5 skip by design (Vision feature print
# is unavailable on the simulator and needs a real device)
make run # opens the project in Xcode; an iOS app launches from there, not the CLIThe Python data pipeline under DataPipeline/ is dev-time only, is not shipped in the app,
and has its own fast suite:
python3 -m pytest DataPipeline/test_pipeline.py # 5 tests, <1sSee IMPLEMENTATION-ROADMAP.md for full phase details and verification checklist.
Current phase: Phase 3: Confidence UI + Polish (Phases 0–2 complete)
- Use
guard letortry?with explicit fallback — force-unwraps (!) only insidefatalError/precondition - File naming: PascalCase for Swift types and files, camelCase for variables
- Architecture: feature-based folder structure (
Features/Camera/,Features/Matching/, etc.) - Async: Swift async/await only — no Combine, no callbacks
- Open
photos.dbas read-onlyDatabasePool(GRDB); write user data elsewhere - Preprocess images to grayscale before
VNGenerateImageFeaturePrintRequest(Vision requirement) - No third-party analytics or crash reporting SDKs in v1
photos.dbis a read-only bundled asset (~80–200MB); opening it writable corrupts the bundle- Keep all user photos, location data, and usage telemetry strictly on-device — no off-device transmission
- Request camera/location permissions only when the user first taps camera/gallery, not on app launch
- Phase 0 scope gate: data pipeline + SQLite index only; no UI; expand beyond 2 cities only after density audit passes (≥25% of 100m grid cells covered)
| Decision | Choice | Rationale |
|---|---|---|
| Index approach | Bundled SQLite (photos.db, ~80–200MB) |
Live API per photo = 2–4s added latency + offline broken |
| NYC photos source | OldNYC dataset (GitHub, ~25K geolocated NYPL photos) | NYPL Space/Time archived Oct 2024; OldNYC has same photos with GPS coords |
| Vision role | Re-ranking only (not primary filter) | Vision needs thumbnails downloaded first; can't cold-filter |
| Heading filter | ±45° window | Magnetometer error in urban canyons can reach ±40°; ±30° drops valid matches |
| iOS minimum | iOS 17 | VNFeaturePrintObservation 768-dim normalized vectors require iOS 17 |
| Composite score | GPS/heading 70% + Vision 30% | Historical photos are stylistically dissimilar; Vision alone unreliable |
| V1 cities | NYC, SF, Chicago, DC, New Orleans, Boston | Highest OldNYC + Wikimedia photo density with GPS metadata |
| Monetization | Free, no paywall | Viral sharing is the growth mechanic — paywalls kill it |
| Design language | "Archival plate" — all color/type tokens live in Afterimage/DesignSystem/Theme.swift |
One subject-derived identity: silver-gelatin darks, museum-label bone, albumen sepia reserved for the historical layer's voice (datelines, era chips, handle ring) |
| Color scheme | Dark-only (.preferredColorScheme(.dark) at root) |
Camera-first app set in the plate archive's dark; system chrome must match |
Afterimage is a free iOS app (iPhone-only) that matches a photo you take — or select from your camera roll — to a geolocated historical photograph from the same location. The core interaction is a draggable vertical slider revealing the historical image beneath the present-day photo. All matching happens on-device against a bundled SQLite index; no backend, no accounts.
Phase 3: Confidence UI + Polish (Phases 0–2 complete) See IMPLEMENTATION-ROADMAP.md for full phase details and verification checklist.
- Language: Swift 5.10+
- UI: SwiftUI (iOS 17+ minimum — no UIKit views except AVFoundation camera wrapper)
- Database: SQLite via GRDB.swift 7.x — typed Swift wrappers, fast spatial queries
- Image loading: Kingfisher 8.x — async fetch + disk cache for thumbnails
- Image ML: Vision framework (
VNGenerateImageFeaturePrintRequest) — on-device feature print similarity - Location: CoreLocation (CLLocationManager + CLHeading)
- Camera: AVFoundation (photo capture pipeline)
- Data pipeline: Python 3.12 + aiohttp + sqlite3 (dev-time only, not shipped)
- Swift: no force-unwraps (
!) outside of fatalError/precondition; useguard letortry?with explicit fallback - File naming: PascalCase for Swift types and files, camelCase for variables
- Architecture: feature-based folder structure (Features/Camera/, Features/Matching/, etc.)
- No third-party analytics or crash reporting SDKs in v1
- All async work via Swift async/await — no Combine, no callbacks
- GRDB: always open
photos.dbas read-onlyDatabasePool - Vision: always preprocess images to grayscale before
VNGenerateImageFeaturePrintRequest
- Do not add features not in the current phase of IMPLEMENTATION-ROADMAP.md
- Do not open
photos.dbas writable — it is a read-only bundled asset; never write user data to it - Do not transmit user photos, location data, or any usage telemetry off-device
- Do not request camera or location permissions on app launch — only when the user first taps camera/gallery
- Do not run
VNGenerateImageFeaturePrintRequeston color images — always convert to grayscale first - Do not use Combine or callback-based async — async/await only
- Do not add UI in Phase 0 — Phase 0 is data pipeline and SQLite index only
- Do not widen Phase 0 to more than 2 cities until density audit passes (≥25% of 100m grid cells covered)
Use this context plus the README and supporting docs to resume the next active task, then promote the repo beyond minimum-viable by capturing a dedicated handoff, roadmap, or discovery artifact.
Prior lessons, decisions, and context for this project live in SecondBrain at wiki/maps/projects/afterimage.md. The whole vault is searchable via the engraph MCP — query it for this project + its stack before non-trivial work.