① 다이나믹 아일랜드 멈춤(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
68 lines
2.7 KiB
Swift
68 lines
2.7 KiB
Swift
//
|
|
// HealthKitCalls.swift
|
|
// Keging
|
|
//
|
|
// HealthKit completion API의 async 래퍼 — iOS(사후 빌더)·워치(라이브 빌더) 공용
|
|
//
|
|
|
|
import HealthKit
|
|
|
|
extension HealthWorkoutType {
|
|
var activityType: HKWorkoutActivityType {
|
|
switch self {
|
|
case .other: .other
|
|
case .coreTraining: .coreTraining
|
|
}
|
|
}
|
|
}
|
|
|
|
nonisolated enum HealthKitCalls {
|
|
static func beginCollection(_ builder: HKWorkoutBuilder, at date: Date) async throws {
|
|
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
|
|
builder.beginCollection(withStart: date) { _, error in
|
|
if let error { continuation.resume(throwing: error) } else { continuation.resume() }
|
|
}
|
|
}
|
|
}
|
|
|
|
static func endCollection(_ builder: HKWorkoutBuilder, at date: Date) async throws {
|
|
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
|
|
builder.endCollection(withEnd: date) { _, error in
|
|
if let error { continuation.resume(throwing: error) } else { continuation.resume() }
|
|
}
|
|
}
|
|
}
|
|
|
|
static func add(_ samples: [HKSample], to builder: HKWorkoutBuilder) async throws {
|
|
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
|
|
builder.add(samples) { _, error in
|
|
if let error { continuation.resume(throwing: error) } else { continuation.resume() }
|
|
}
|
|
}
|
|
}
|
|
|
|
static func finishWorkout(_ builder: HKWorkoutBuilder) async throws -> HKWorkout? {
|
|
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<HKWorkout?, Error>) in
|
|
builder.finishWorkout { workout, error in
|
|
if let error { continuation.resume(throwing: error) } else { continuation.resume(returning: workout) }
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 운동 강도(1~10) 샘플을 운동에 연결 — 애플 운동 앱의 강도 기록과 같은 데이터
|
|
@available(iOS 18.0, watchOS 11.0, *)
|
|
static func relateEffort(store: HKHealthStore, workout: HKWorkout, score: Int) async throws {
|
|
let sample = HKQuantitySample(
|
|
type: HKQuantityType(.workoutEffortScore),
|
|
quantity: HKQuantity(unit: .appleEffortScore(), doubleValue: Double(min(max(score, 1), 10))),
|
|
start: workout.startDate,
|
|
end: workout.endDate
|
|
)
|
|
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
|
|
store.relateWorkoutEffortSample(sample, with: workout, activity: nil) { _, error in
|
|
if let error { continuation.resume(throwing: error) } else { continuation.resume() }
|
|
}
|
|
}
|
|
}
|
|
}
|