mycode/myApp/Keging/IOS/HealthRecorder.swift
songyc macbook f5f8d3bde4 fix(keging): 실기기 1차 피드백 3건 — DI 백그라운드 멈춤·진동 패턴 동일·칼로리 0
① 다이나믹 아일랜드 멈춤(2중 방어):
- 오디오 키퍼 하드닝 — volume 0.1(0.0은 유지 풀림 사례)·prepareToPlay·인터럽션/씬 전환 revive()
- 엔진을 벽시계 기준으로 재설계 — 전 국면이 sessionStart로부터 결정적(computeTarget),
  타이머는 경계 재장전용, scenePhase .active에서 resyncAfterWake()로 정확한 국면 자가 교정

② 진동 패턴 동일(톡=톡톡): 시스템 바이브는 재생 중 재호출이 무시됨(실측) —
  시간 오프셋 방식 폐기, AudioServicesPlaySystemSoundWithCompletion 완료 체이닝으로 재작성
  (톡 1방 / 톡톡 0.25s 쉬고 1방 / 드르륵 0.15s 3연타 / 지잉 즉시 이어붙인 3방)

③ 운동 칼로리 0: 빌더 운동은 에너지 샘플 직접 추가 필요 —
  iOS는 MET 추정(1.8+강도×0.17, 70kg 가정) activeEnergyBurned 샘플 add,
  워치는 심박·활성 에너지 읽기 권한 추가로 센서 실측 수집, 권한 문구 3언어 갱신

검증: Debug·Release·워치 빌드 그린, 완주 경로 시뮬 재검증(엔진 재설계 후).
DI 갱신·진동 구분·칼로리는 실기기 재확인 필요(재실행 시 '활성 에너지' 권한 시트 뜸)

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

77 lines
3.3 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// HealthRecorder.swift
// Keging
//
// '' (iOS ).
// (workout effort score, 1~10) .
// · · .
//
import HealthKit
@MainActor
final class HealthRecorder {
static let shared = HealthRecorder()
private let store = HKHealthStore()
private var authRequested = false
private init() {}
private static var shareTypes: Set<HKSampleType> {
[
HKObjectType.workoutType(),
HKQuantityType(.workoutEffortScore),
HKQuantityType(.activeEnergyBurned),
]
}
/// MET 1.8( 0)3.5( 10), 70kg .
/// 0kcal ( )
static func estimatedKcal(seconds: TimeInterval, effortScore: Int) -> Double {
let met = 1.8 + Double(min(max(effortScore, 0), 10)) * 0.17
return met * 3.5 * 70 / 200 * (seconds / 60)
}
/// ( )
func requestAuthorizationIfNeeded() {
#if DEBUG
// QA
if CommandLine.arguments.contains("-noHealth") { return }
#endif
guard HKHealthStore.isHealthDataAvailable(), !authRequested else { return }
authRequested = true
store.requestAuthorization(toShare: Self.shareTypes, read: nil) { _, _ in }
}
/// , (/ )
func save(record: SessionRecord, effortScore: Int, workoutType: HealthWorkoutType) {
guard HKHealthStore.isHealthDataAvailable() else { return }
let start = record.startedAt
let end = start.addingTimeInterval(Double(record.reps * (record.contractSeconds + record.relaxSeconds)))
let configuration = HKWorkoutConfiguration()
configuration.activityType = workoutType.activityType
let builder = HKWorkoutBuilder(healthStore: store, configuration: configuration, device: .local())
Task { @MainActor in
do {
try await HealthKitCalls.beginCollection(builder, at: start)
let kcal = Self.estimatedKcal(seconds: end.timeIntervalSince(start), effortScore: effortScore)
let energySample = HKQuantitySample(
type: HKQuantityType(.activeEnergyBurned),
quantity: HKQuantity(unit: .kilocalorie(), doubleValue: kcal),
start: start,
end: end
)
try await HealthKitCalls.add([energySample], to: builder)
try await HealthKitCalls.endCollection(builder, at: end)
let workout = try await HealthKitCalls.finishWorkout(builder)
if effortScore > 0, let workout {
try? await HealthKitCalls.relateEffort(store: store, workout: workout, score: effortScore)
}
} catch {
//
}
}
}
}