# ScopeDOPE iOS — Phased Build Plan

**Status:** active · **Created:** 2026-06-14 · **Owner:** Cold Bore Ballistics LLC

**Locked decisions**
- **Engine:** build an XCFramework from the Kotlin Multiplatform `:ballistics` engine
  (not a from-scratch Swift re-implementation) to guarantee Android output parity.
- **Environment:** macOS + Xcode to be acquired. Until then, Windows-side prep only
  (engine source edits, model/`.dope` design, planning) — no iOS binary can be produced
  off a Mac.
- **Independence (hard constraint):** everything for the iOS build is **new and
  self-contained inside `iOS/`**. No file outside `iOS/` is modified, and nothing here
  build-depends on `scopedope-android-main/` or `scopedope-shared-main/`. Reference code
  is **vendored** (copied in and maintained independently), never linked.

> The four-file ScopeDOPE SPEC and the engine white paper remain the functional
> reference for *what* to build. This plan covers *how and in what order*.

---

## Target architecture (mirrors the Android layering, natively in Swift)

```
SwiftUI Views               ← portrait-only, dark-only, instrument theme
   ↓ observe
ViewModels (@Observable)    ← SessionViewModel, PrinterViewModel, SensorViewModel, per-screen VMs
   ↓ call
Use Cases (Swift)           ← CalculateDopeUseCase, SavePngUseCase, …
   ↓ call
Repository protocols        ← AmmoRepository, WeatherSensorSource, PrinterSource, …
   ↓ implemented by
Data layer                  ← GRDB (ammo), Codable (.dope), CoreBluetooth, CoreLocation,
                              FileManager, UserDefaults
   ↓ math via
ScopeDopeBallistics.xcframework   ← vendored KMP :ballistics engine, built in iOS/engine/
```

- **Project home:** a SwiftUI app + the engine module, both **inside `iOS/`**.
- **DI:** manual composition root (no Hilt). Android `DatabaseModule`/`RepositoryModule`
  are the wiring reference only.
- **Concurrency:** Swift `async/await` + `@Observable`/Combine in place of
  Coroutines/StateFlow.
- **Vendoring rule:** the ammo DB, `.dope` schema, validation test data, fonts, and brand
  assets are each copied into `iOS/` as independent inputs when their phase begins.

---

## Phase 0 — Engine iOS-readiness & XCFramework  ✅ *(initial cut done; XCFramework pending Mac)*

Goal: a Swift-callable engine producing bit-identical results to Android.

| Step | State | Notes |
|---|---|---|
| Vendor `:ballistics` commonMain into `iOS/engine/` | **done** | Independent copy; originals untouched |
| Replace `java.lang.Math.toRadians` (7 sites) with multiplatform `toRadians()` | **done** | `kotlin.math.PI`; identical numerics |
| Add `iosArm64/iosX64/iosSimulatorArm64` + XCFramework; drop Android target/SDK | **done** | `engine/ballistics/build.gradle.kts` |
| `./gradlew :ballistics:jvmTest` green after edits | **pending** | Runs on any OS; first checkpoint |
| `assembleScopeDopeBallisticsXCFramework` | **pending (macOS)** | Produces `ScopeDopeBallistics.xcframework` |
| iOS smoke test: 3 known cases vs. JVM (< 0.01 mil) | **pending (macOS)** | Proves Native == JVM |
| Vendor the full 39-case validation corpus + a Swift/KMP harness | **later** | Optional but recommended parity gate |

**Risk:** Kotlin→Swift interop surfaces `List<Pair<…>>` / `Map<Double,…>` awkwardly. Add a
thin Swift adapter exposing clean Swift types over the generated framework API.

---

## Phase 1 — Data models & persistence  *(mostly Windows-authorable)*

| Workstream | Android reference (read-only) | iOS approach (vendored/independent) |
|---|---|---|
| Domain models | `domain/model/Models.kt` | Swift structs: `AmmoLoad`, `DopeEntry`, `ConditionsAtMeasurement`, `RangeConditions`, `WindConditions`, `ResultRow`, `LabelEntry` |
| Built-in ammo DB | `ammo_builtin.db` + Room schema `4.json` | ✅ **vendored** to `iOS/assets/ammo/` (DB + CSV + build script + v4 schema). Open read-only with **GRDB**; schema mirrors Room v4 |
| User ammo DB | `UserAmmoDatabase` + migrations | Separate writable GRDB DB; replicate v1→v4 migrations; **no destructive fallback** |
| Preferences | `AppPreferences.kt` + SPEC App. D | `UserDefaults` wrapper; replicate every key/default |
| `.dope` format | `DopeSessionSerializer.kt` + `dope-format` schema | `Codable` structs for v3; round-trip parity tests |

Recommend obtaining the `dope-format` JSON Schema and **copying it into `iOS/`** before
finalizing serialization, so the contract is authoritative rather than inferred.

---

## Phase 2 — Headless calculation pipeline  *(logic Windows-authorable; runs on Mac)*

Port `CalculateDopeUseCase`: the two-phase bias cache (`computeEntryBiases` →
`interpolateDopeAllFromBiases`), MV temperature adjustment, and `toClicks`. Headless test
mirrors the Android `CalculateDopeUseCaseTest` and asserts identical `ResultRow`s.
**Exit:** ammo + DOPE + conditions → results matches Android, no UI.

---

## Phase 3 — Core UI: the four tabs  *(macOS)*

Design system first (from `ui/theme/*` + `UI_DESIGN_STYLE_GUIDE.md`): color/type tokens,
`InstrumentPanel`, `ActionButton`/`NotchedCornerShape`, `NumericField`, `LabeledValue`,
custom bottom nav, session-name strip. Lock portrait + dark in `Info.plist`.

Port order (each wired to live calculation as it lands): **DOPE → Conditions → Results →
Print** (preview stub until Phase 5).

---

## Phase 4 — Hardware: BLE + compass  *(macOS + devices for full test)*

CoreBluetooth `CBCentralManager` central; port the three drivers from their documented
protocols (SPEC §9, §12) and the Android driver code:
- **Niimbot B1** printer (UART-over-BLE; full opcode sequence in SPEC §12.1)
- **WeatherFlow Tactical** (16-byte frame) and **BTMETER BT-100** (14-byte) sensors
- **Phone compass** wind direction via CoreLocation heading / CoreMotion (replaces
  Android rotation-vector); declination via `CLLocation`
- GPS altitude via CoreLocation; map Android runtime permissions to `Info.plist`
  usage strings (`NSBluetoothAlwaysUsageDescription`, location).

Driver logic can be authored early; physical hardware needed only for end-to-end verify.

---

## Phase 5 — Label rendering, PNG, print  *(macOS)*

Port `CircleLabelRenderer` + `RectLabelRenderer` (19 formats, `LabelFormats.kt`) to
**Core Graphics** at the printer's native DPI; reuse rotation + 1-bit threshold pipeline
(SPEC §11.7, §12.1). Wire the Niimbot print path; PNG export to **`PHPhotoLibrary`**.
Parity check: render identical sessions on both platforms and diff bitmaps.

---

## Phase 6 — Sessions  *(macOS)*

`.dope` Save/Load/Share/History via `UIDocumentPickerViewController` +
`UIActivityViewController`; recent-sessions list (max 20) in `UserDefaults`.
Cross-platform round-trip: load Android-saved `.dope` files unchanged and vice-versa.

---

## Phase 7 — Onboarding, settings, feedback, crash  *(macOS)*

Splash (+ beta watermark / 45-day expiry), 4-screen onboarding, 9-slide tutorial, Basic +
Advanced Settings, Feedback/Support (`mailto`), **Sentry Cocoa** crash reporting —
**opt-in, OFF by default**, first-run consent, no-PII constraints (SPEC §16.1). In-app
review after first successful print (`SKStoreReviewController`).

---

## Phase 8 — Parity validation & release  *(macOS)*

Full cross-platform parity sweep (engine < 0.01 mil, identical `.dope` round-trips, label
bitmaps), device testing of BLE/print, performance targets (cold launch < 1.5 s, calc
< 100 ms), then **TestFlight** internal build per `ScopeDOPE_SDLC.md` §7.

---

## Critical path

```
Phase 0 (engine XCFramework) ─┬─> Phase 2 (calc) ─> Phase 3 (UI) ─┬─> Phase 5 (labels/print) ─> Phase 8
Phase 1 (data/models) ────────┘                                   ├─> Phase 6 (sessions)
                                                Phase 4 (BLE) ─────┘
                                                Phase 7 (onboarding/crash) ── parallelizable
```

**Hard prerequisites before coding in earnest:** (1) macOS + Xcode + Kotlin toolchain;
(2) the SwiftUI app scaffold created inside `iOS/`; (3) the `dope-format` schema vendored
into `iOS/` for Phase 1. Phase 0's engine edits and Phase 1's model/`.dope` design are the
only items meaningfully completable on Windows today — and Phase 0's edits are already in.

---
© 2026 Cold Bore Ballistics LLC. Proprietary and Confidential.
