Back to RunWay
Under The Hood

Four Design Decisions

Concurrency, state management, cross-device sync, and GPWS logic - the
four areas I thought hardest about while building RunWay.

CoreLocation
RunningCenter (actor · serial queue)
AsyncStream<FlightData>
HealthKit + WatchConnectivity
send / receive
ViewModel (@Observable) → SwiftUI
Why it's designed this way

“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.”

SwiftUIUI
@MainActor + @ObservableState management
RunningCenter (Actor)Concurrency
HealthCenter (Actor)Thread-safe heart rate/cadence storage
AsyncStreamSensors → ViewModel
FlightPhase enumState machine
WatchConnectivityiPhone ↔ Watch
SwiftDataStorage
CloudKit (CKRecord)Manual backup/restore · Private Database
Douglas-PeuckerGPS route compression, tolerance scaled to route diagonal
MapKit + MapPolylineRoute visualization
HealthKitHeart rate, cadence, workout session
WidgetKit + ActivityKitDynamic Island
WeatherKitLive weather
AVSpeechSynthesizerVoice announcements for km splits and countdown
CombineLocation, alert, session-state publishers + timer
System Flow

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.

Click a box to see its real code on the right
01 · Sensor Input02 · Core Compute (actor)03 · ViewModel · Cross-Device Sync04 · Live Screens05 · Persistence06 · HistoryLocationServiceSERVICEHealthCenterACTORAltimeterServiceSERVICERunningCenterACTORRunViewModelACTORWatchConnectivityServiceSERVICEWatchViewModelACTORPFDViewVIEWWatchGPWSViewVIEWWatchPFDViewVIEWSwiftData ModelsMODELCloudBackupServiceSERVICERunReminderServiceSERVICELogbookViewVIEWFlightSummaryViewVIEWAlertsViewVIEWFlightCalendarViewVIEW
RunningCenter
RunWay/Shared/Actor/RunningCenter.swift

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}
Responsibilities
  • ▸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
Why It's Built This Way

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.