- 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
74 lines
2.7 KiB
Swift
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)
|
|
}
|
|
}
|
|
}
|