mycode/myApp/Keging/IOS/PhoneSync.swift
songyc macbook 8f634c83f4 fix(keging): 실기기 3차 피드백 6건 — DI 멈춤 근본 원인(햅틱 엔진 세션 충돌) 제거·워치 세팅 pull 동기화·완료 진동·하루 타임라인·칼로리 정합
- CHHapticEngine을 공유 AVAudioSession에 부착(기본 생성 시 독자 오디오 정책이
  백그라운드 무음 루프를 끊던 것이 DI 멈춤 원인) + 오디오 키퍼 하드닝(루트 변경·
  미디어 서버 리셋·ensureAlive) + 운동 중 2초 하트비트(오디오·엔진·LA 자가 치유)
- Live Activity: 실제 phaseStart 사용·동일 상태 no-op·staleDate 8초·isStale 흐림
- 워치 세팅 동기화 3중화: 변경 push + reachability 재푸시 + 워치 앱 열릴 때 sendMessage pull
- 완주 진동 신설: 폰 2.4초 연속(백그라운드 6방 체인, 오디오 정지 4초 지연), 워치 retry×2+success
- 통계 하루 탭 맨 위 '오늘의 타임라인'(24시간 축 점 그래프, 시각 라벨·지금 룰)
- 폰 칼로리 공식을 애플워치 '기타' 운동 규칙(빠르게 걷기 상당)에 정합: MET 4.0+강도×0.15
- 도움말 4건 갱신·en/ja 번역·CFBundleName 보충, CLAUDE.md 현행화

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPmp29DQHY6Ahfss5SRopT
2026-09-02 14:30:08 +09:00

74 lines
2.7 KiB
Swift

//
// PhoneSync.swift
// Keging
//
// (iOS ) ,
//
import WatchConnectivity
final class PhoneSync: NSObject, WCSessionDelegate {
static let shared = PhoneSync()
private override init() { super.init() }
func activate() {
guard WCSession.isSupported() else { return }
let session = WCSession.default
session.delegate = self
session.activate()
SettingsStore.shared.onConfigChange = { [weak self] config in
self?.pushConfig(config)
}
}
func pushConfig(_ config: WorkoutConfig) {
guard WCSession.isSupported(), WCSession.default.activationState == .activated else { return }
guard let data = try? JSONEncoder().encode(config) else { return }
try? WCSession.default.updateApplicationContext(["config": data])
}
// MARK: WCSessionDelegate
nonisolated func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
guard activationState == .activated else { return }
Task { @MainActor in
PhoneSync.shared.pushConfig(SettingsStore.shared.config)
}
}
nonisolated func sessionDidBecomeInactive(_ session: WCSession) {}
nonisolated func sessionDidDeactivate(_ session: WCSession) {
session.activate()
}
///
nonisolated func sessionReachabilityDidChange(_ session: WCSession) {
guard session.isReachable else { return }
Task { @MainActor in
PhoneSync.shared.pushConfig(SettingsStore.shared.config)
}
}
///
nonisolated func session(_ session: WCSession, didReceiveMessage message: [String: Any], replyHandler: @escaping ([String: Any]) -> Void) {
guard message["request"] as? String == "config" else {
replyHandler([:])
return
}
Task { @MainActor in
let data = (try? JSONEncoder().encode(SettingsStore.shared.config)) ?? Data()
replyHandler(["config": data])
}
}
nonisolated func session(_ session: WCSession, didReceiveUserInfo userInfo: [String: Any] = [:]) {
guard let data = userInfo["record"] as? Data,
let record = try? JSONDecoder().decode(SessionRecord.self, from: data) else { return }
Task { @MainActor in
RecordStore.shared.append(record)
}
}
}