Four Design Decisions
Concurrency, state management, cross-device sync, and GPWS logic - the
four areas I thought hardest about while building RunWay.
“GPS updates arrive continuously and require recalculating pace, distance, and GPWS status each time, so I isolated that calculation logic inside a RunningCenter Actor to guarantee state integrity. Heart rate and cadence come in through WatchConnectivity into a HealthCenter Actor that just holds the values thread-safely with no judgment logic of its own, and RunningCenter pulls from it whenever it needs those values for the @Observable ViewModel.”
Full Systems Map
From GPS, heart rate and altitude sensors through the screens, storage, and
iPhone-Watch sync, all 17 components that make up the app on one map.
The center of every running calculation. Takes GPS/heart rate/altitude in, computes distance, pace, GPWS, and splits, and streams out FlightData.
1actor RunningCenter {2 private(set) var totalDistance: Double = 03 private(set) var gpwsStatus: GPWSState = .normal4 private(set) var isStationary: Bool = false56 func processLocation(_ location: CLLocation) async {7 if location.speedAccuracy >= 0 {8 }9 continuation?.yield(flightData)10 }11}- ▸Accumulates distance, smooths pace with a double exponential moving average
- ▸Raw-speed-based stationary detection (isStationary), gated by speedAccuracy confidence
- ▸GPWS (SINK RATE/OVERSPEED/MINIMUMS) judgement, reset to normal while stationary
- ▸Records km splits, smooths and filters outliers in relative altitude
iOS and Watch each receive GPS, heart rate, and altitude independently, so RunningCenter also runs as a separate instance per device. Stationary detection confirms only after filtering out samples with low GPS confidence (speedAccuracy). At first that check only gated stationary detection, not the speed smoothing used for pace. The moment you slow down hard in response to OVERSPEED is exactly when GPS confidence tends to go negative, so stationary detection never confirmed while the smoothed speed kept getting pulled toward zero, inflating pace until the app crashed. Fixed in 1.3.1 by moving the smoothing update inside the same confidence check.