RunWay 홈으로
Under The Hood

설계 결정 네 가지

동시성 처리, 상태 관리, 기기 간 동기화, GPWS 판정 로직 - RunWay를
만들며 가장 많이 고민한 네 가지 지점입니다.

CoreLocation
RunningCenter (actor · serial queue)
AsyncStream<FlightData>
HealthKit + WatchConnectivity
send / receive
ViewModel (@Observable) → SwiftUI
왜 이렇게 설계했는가

“GPS 업데이트가 연속적으로 들어오면서 페이스, 거리, GPWS 판정까지 매번 계산해야 하므로, 이 계산 로직을 RunningCenter Actor로 격리해 상태 무결성을 보장했습니다. 심박수와 케이던스는 WatchConnectivity가 받아서 판정 로직 없이 값만 스레드 안전하게 들고 있는 HealthCenter Actor에 넘기고, RunningCenter가 필요할 때 그 값을 가져가 @Observable ViewModel로 전달합니다.”

SwiftUIUI
@MainActor + @Observable상태관리
RunningCenter (Actor)동시성
HealthCenter (Actor)심박수/케이던스 스레드 안전 보관
AsyncStream센서 → ViewModel
FlightPhase enum상태 머신
WatchConnectivityiPhone ↔ Watch
SwiftData저장
CloudKit (CKRecord)수동 백업/복원 · Private Database
Douglas-Peucker경로 대각선 비례 오차로 GPS 좌표 압축
MapKit + MapPolyline경로 시각화
HealthKit심박수·케이던스·운동 세션
WidgetKit + ActivityKitDynamic Island
WeatherKit실시간 날씨
AVSpeechSynthesizerkm 스플릿·카운트다운 음성 안내
Combine위치·알림·세션 상태 Publisher + Timer
System Flow

전체 시스템맵

GPS·심박·고도 센서부터 화면, 저장, 아이폰↔워치 동기화까지 실제 앱을 구성하는
17개 컴포넌트와 그 사이를 오가는 데이터를 한 장으로 그렸습니다.

박스를 클릭하면 오른쪽에 실제 코드가 뜹니다
01 · 센서 입력02 · 핵심 연산 (actor)03 · ViewModel · 기기간 동기화04 · 실시간 화면05 · 저장06 · 기록 열람LocationServiceSERVICEHealthCenterACTORAltimeterServiceSERVICERunningCenterACTORRunViewModelACTORWatchConnectivityServiceSERVICEWatchViewModelACTORPFDViewVIEWWatchGPWSViewVIEWWatchPFDViewVIEWSwiftData ModelsMODELCloudBackupServiceSERVICERunReminderServiceSERVICELogbookViewVIEWFlightSummaryViewVIEWAlertsViewVIEWFlightCalendarViewVIEW
RunningCenter
RunWay/Shared/Actor/RunningCenter.swift

모든 러닝 계산의 중심. GPS/심박/고도를 받아 거리·페이스·GPWS·스플릿을 계산해 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}
담당 기능
  • ▸누적 거리 계산, 이중 지수 이동평균(Double EMA)으로 페이스 스무딩
  • ▸raw 속도 기반 정지 판단(isStationary), speedAccuracy 신뢰도 체크
  • ▸GPWS(SINK RATE/OVERSPEED/MINIMUMS) 판정, 정지 중엔 normal로 리셋
  • ▸km 단위 스플릿 기록, 상대 고도 스무딩과 이상치 필터링
왜 이렇게 했는가

iOS와 Watch는 각자 GPS·심박·고도를 따로 받기 때문에, RunningCenter도 기기마다 독립된 인스턴스로 돈다. 정지 판단은 GPS 신뢰도(speedAccuracy)가 낮은 샘플을 걸러가며 확정하는데, 처음엔 이 체크가 정지 판단에만 걸려있고 페이스 계산용 속도 스무딩에는 안 걸려있었다. 오버스피드에 반응해 급감속하는 순간 신뢰도가 자주 음수로 찍히는 바람에, 정지 확정은 안 되는데 스무딩 값만 계속 낮아져 페이스가 무한정 부풀다 앱이 죽는 실사용 버그로 이어졌다. 스무딩 갱신을 같은 신뢰도 체크 안으로 옮기는 것으로 1.3.1에서 해결했다.