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
| 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
- 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).
- Repo: sibling
sailever/sailever-watchos/ with
SailEver Watch App (Xcode → New Project → watchOS → App → Watch-only App)
Packages/SailEverCore (Swift package, Swift Testing)
- Capabilities: HealthKit; Background Modes → Workout processing. Info.plist:
NSHealthShareUsageDescription, NSHealthUpdateUsageDescription, NSLocationWhenInUseUsageDescription, NSMotionUsageDescription (barometer).
- First commit: the SLTB encoder in
SailEverCore passing the golden-vector tests. Pure logic, fast with swift test, and the contract with the server.
- Simulator loop:
- The watchOS simulator reaches
http://localhost:3000 directly, unlike a Garmin device: run bin/dev and upload to it.
- Fake GPS: Simulator → Features → Location, or a GPX file in the scheme.
- CLI build:
xcodebuild -scheme "SailEver Watch App" -destination 'platform=watchOS Simulator,name=Apple Watch Ultra 3 (49mm)' build.
- The simulator has no barometer and meaningless battery numbers, hence Phase 0 on a real watch.
- Real watch: HTTPS required; reuse the
cloudflared tunnel from the Garmin README.