Skip to content

SailEver — Apple Watch Port Plan (native watchOS)

Date: 2026-10-08 Status: Draft — proposal Target: new repo sailever-watchos/, Swift + SwiftUI, watch-only app. Counterpart of apps/garmin-watchapp (Garmin). Same sync contract: track-sync.md.


1. Navigation

1.1 The proposed inputs, checked against watchOS

Proposal Possible on watchOS? Decision
Swipe up/down between views Yes. Standard since watchOS 10 (TabView + .verticalPage); turning the Digital Crown pages as well. Keep. Dashboard → Track → Environment by swipe or crown.
Press the crown for the action menu No. A crown press always goes to the system (home / watch face); apps can't intercept it. Action menu becomes a Controls page on the left (swipe right), as in Apple's Workout app.
Double press for quick vessel state change No. Crown double press (last app) and side button double click (Wallet) are reserved. Double Tap hand gesture (Series 9+, Ultra 2+) and the Action button on Ultra.

1.2 Layout

         ◄ swipe right
┌────────────┐   ┌──────────────────────────┐
│ CONTROLS   │   │ INSTRUMENTS (vertical)   │
│ Pause      │   │  ▲ swipe / crown         │
│ Stop       │◄─►│  1 Dashboard (SOG hero)  │
│ Stats      │   │  2 Track plot            │
│ Vessel st. │   │  3 Environment           │
│ Water Lock │   │  ▼                       │
│ Discard    │   └──────────────────────────┘
└────────────┘
  • Vessel state (the main on-deck action), three ways in, since no single input exists on every model:
  • Tap the state header on the Dashboard → picker, previous state preselected.
  • Double Tap (pinch) on any instrument page: first opens the picker, second confirms the highlighted state. One hand stays on the tiller, the screen isn't touched. .handGestureShortcut(.primaryAction).
  • Action button (Ultra), bound to a SailEver App Intent.
  • Double Tap does not cycle blindly through the 8 states: a wrong state goes unnoticed and corrupts the sail/engine split.
  • Track view zoom: the crown pages, so it can't also zoom. On-screen +/− buttons, or tap the map to put the crown into zoom mode until the next tap.
  • Pause view: maps to the Always-On / reduced-luminance style (isLuminanceReduced): black screen, time, battery, "paused".
  • Stop: watchOS apps can't call exit() (App Review rejects it). End the workout, show the save view, return to the start list.
  • Start list: Upload tracks (only while files are left), Resume trip, Start new trip, Pair device / Unpair device, Settings, in a NavigationStack.

1.3 What Apple users will care about

  • Wet screen: spray and rain make touch unreliable. Water Lock sits on the Controls page; every instrument works without any input. Whether Double Tap works under Water Lock must be tested on the water.
  • Battery: a regular Apple Watch lasts about a day of GPS recording; Ultra much longer, especially in workout low-power mode. Multi-day passages are a real limit; Phase 0 measures it.
  • Haptics are weaker than Garmin vibes. Alerts (float, baro, battery) use the strongest patterns, repeated, plus a full-screen overlay.

2. Architecture: port the logic, rebuild the UI

The Monkey C code (~6,300 lines) isn't reusable as such; the domain logic maps almost 1:1.

Garmin (today) watchOS native
FIT SAILING activity (fallback copy) HKWorkoutSession + HKLiveWorkoutBuilder, activity type .sailing, saved to Apple Health with an HKWorkoutRoute. Garmin also has Boating trips (lifecycle.md); HealthKit has no boating type, so decide during the port what a Boating trip records (parity P14).
FIT session fields trip_token, app_version Metadata on the HKWorkout
Record field vessel_state HKWorkoutEvent segment events with metadata, plus the SLTB stream (SLTB stays the source of truth)
Position CLLocationUpdate.liveUpdates() (async), kept alive by the workout session
Barometer CMAltimeter (pressure in kPa)
TripStore (zero data loss) Files in Application Support + HKHealthStore.recoverActiveWorkoutSession after crash / relaunch
TrackQueue (SLTB in Storage) Real files, as the spec says for other platforms
SyncEngine (one attempt, no retries) URLSession, same rules for spec parity
Device token in Application.Storage Keychain
SunCalc, MoonCalc, BaroIndicator, CogCalculator, AlertManager, auto-stats, TripContext Plain Swift in the SailEverCore package, unit-tested on macOS
  • Server: the SLTB API stays as is. Likely only change: store platform / model at pairing.
  • iPhone app: not needed for v1 (independent watch apps are allowed). A companion can come later.
  • Minimum OS: watchOS 11+ (Series 6+, SE 2, all Ultras). Double Tap needs Series 9 / Ultra 2 or newer, so it is a shortcut, never the only path.

3. Phases

Phase Content Exit criterion
0. Spike (1–2 wks) Sailing workout + GPS at 5 s + barometer on a real watch, on the water Battery per hour measured; Double Tap / Water Lock behaviour known. Go / no-go.
1. Core SailEverCore: vessel state, SLTB encoder (golden vectors from docs/sltb/sltb.py), sun/moon, baro trend, COG / turn detection Golden vectors byte-exact
2. Recording Trip / track state machine, pause / resume, crash recovery, HealthKit save Killing the app mid-track loses nothing
3. UI Start list, 3 instrument pages, Controls page, vessel state picker, 2-page stats, Always-On layout Usable on 41 mm and 49 mm
4. Sync Pairing (6-digit code), segment upload, leftover Upload tracks, tides from the response, 401 → unpair E2E: track appears on the web
5. Alerts & settings Float / battery / baro alerts, distance / time splits, settings Matches the Alerts & Auto-Stats section of apps/garmin-watchapp/README.md
6. Polish & ship Complication / Smart Stack widget (SOG, state), Action button intent, TestFlight, App Store (HealthKit needs a privacy policy) TestFlight build on the water

4. Getting started

  1. Prerequisites: latest Xcode; Apple Developer Program ($99 / year, needed for HealthKit on device and TestFlight); an Apple Watch (ideally Ultra) paired to an iPhone, Developer Mode on (watch: Settings → Privacy & Security).
  2. Repo: sibling sailever/sailever-watchos/ with
  3. SailEver Watch App (Xcode → New Project → watchOS → App → Watch-only App)
  4. Packages/SailEverCore (Swift package, Swift Testing)
  5. Capabilities: HealthKit; Background Modes → Workout processing. Info.plist: NSHealthShareUsageDescription, NSHealthUpdateUsageDescription, NSLocationWhenInUseUsageDescription, NSMotionUsageDescription (barometer).
  6. First commit: the SLTB encoder in SailEverCore passing the golden-vector tests. Pure logic, fast with swift test, and the contract with the server.
  7. Simulator loop:
  8. The watchOS simulator reaches http://localhost:3000 directly, unlike a Garmin device: run bin/dev and upload to it.
  9. Fake GPS: Simulator → Features → Location, or a GPX file in the scheme.
  10. CLI build: xcodebuild -scheme "SailEver Watch App" -destination 'platform=watchOS Simulator,name=Apple Watch Ultra 3 (49mm)' build.
  11. The simulator has no barometer and meaningless battery numbers, hence Phase 0 on a real watch.
  12. Real watch: HTTPS required; reuse the cloudflared tunnel from the Garmin README.