- 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
63 lines
2.3 KiB
Swift
63 lines
2.3 KiB
Swift
//
|
|
// WatchSync.swift
|
|
// KegingWatch Watch App
|
|
//
|
|
// 폰 연동 (워치 측) — 폰에서 세트 구성을 받고, 완주 기록을 폰으로 보낸다
|
|
// (transferUserInfo는 폰이 안 닿아도 자동으로 큐에 쌓였다 전달됨)
|
|
//
|
|
|
|
import WatchConnectivity
|
|
|
|
final class WatchSync: NSObject, WCSessionDelegate {
|
|
static let shared = WatchSync()
|
|
|
|
private override init() { super.init() }
|
|
|
|
func activate() {
|
|
guard WCSession.isSupported() else { return }
|
|
let session = WCSession.default
|
|
session.delegate = self
|
|
session.activate()
|
|
}
|
|
|
|
func sendRecord(_ record: SessionRecord) {
|
|
guard WCSession.isSupported() else { return }
|
|
guard let data = try? JSONEncoder().encode(record) else { return }
|
|
WCSession.default.transferUserInfo(["record": data])
|
|
}
|
|
|
|
/// 폰에 최신 세팅을 요청 — 워치 앱이 열릴 때마다 호출 (폰 앱이 꺼져 있어도 시스템이 깨움).
|
|
/// 실패(폰이 안 닿음 등)는 무시 — applicationContext 경로가 커버한다
|
|
func requestConfigFromPhone() {
|
|
guard WCSession.isSupported() else { return }
|
|
let session = WCSession.default
|
|
guard session.activationState == .activated else { return }
|
|
session.sendMessage(
|
|
["request": "config"],
|
|
replyHandler: { [weak self] reply in self?.applyConfig(from: reply) },
|
|
errorHandler: { _ in }
|
|
)
|
|
}
|
|
|
|
// MARK: WCSessionDelegate
|
|
|
|
nonisolated func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
|
|
guard activationState == .activated else { return }
|
|
applyConfig(from: session.receivedApplicationContext)
|
|
}
|
|
|
|
nonisolated func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String: Any]) {
|
|
applyConfig(from: applicationContext)
|
|
}
|
|
|
|
private nonisolated func applyConfig(from context: [String: Any]) {
|
|
guard let data = context["config"] as? Data,
|
|
let config = try? JSONDecoder().decode(WorkoutConfig.self, from: data) else { return }
|
|
Task { @MainActor in
|
|
if SettingsStore.shared.config != config {
|
|
SettingsStore.shared.config = config
|
|
}
|
|
}
|
|
}
|
|
}
|