mycode/myApp/Keging/Shared/WorkoutSettings.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

160 lines
6.0 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.

//
// WorkoutSettings.swift
// Keging
//
// (··) ,
//
import Foundation
import Combine
/// 3 / .
/// (= / = / = )
nonisolated enum HapticPattern: String, CaseIterable, Identifiable, Codable {
case basic, doubleTap, long
var id: String { rawValue }
var label: String {
switch self {
case .basic: String(localized: "기본")
case .doubleTap: String(localized: "톡톡")
case .long: String(localized: "지이이잉")
}
}
/// Core Haptics ( , , , ) · .
/// (HapticEnginePlayer). 0.28( 0.5 ),
/// 1.2 Core Haptics
var hapticEvents: [(time: Double, duration: Double, intensity: Float, sharpness: Float)] {
switch self {
case .basic: [(0, 0.25, 1.0, 0.55)]
case .doubleTap: [(0, 0.14, 1.0, 0.65), (0.28, 0.14, 1.0, 0.65)]
case .long: [(0, 1.2, 1.0, 0.45)]
}
}
/// (1.0 · 4 )
init?(migrating raw: String?) {
guard let raw else { return nil }
if let direct = HapticPattern(rawValue: raw) {
self = direct
return
}
switch raw {
case "once", "short": self = .basic
case "twice", "thrice", "heartbeat", "rattle": self = .doubleTap
default: return nil
}
}
}
/// . " " .
/// ( Core Haptics · )
nonisolated enum FinishHaptic {
static let events: [(time: Double, duration: Double, intensity: Float, sharpness: Float)] = [
(0, 2.4, 1.0, 0.3)
]
}
/// (HKWorkoutActivityType HealthKitCalls.swift)
nonisolated enum HealthWorkoutType: String, CaseIterable, Identifiable, Codable {
case other, coreTraining
var id: String { rawValue }
var label: String {
switch self {
case .other: String(localized: "기타")
case .coreTraining: String(localized: "코어 트레이닝")
}
}
}
/// = ( ) ×
nonisolated struct WorkoutConfig: Codable, Equatable {
/// (010)
var prepSeconds: Int = 3
var contractSeconds: Int = 10
var relaxSeconds: Int = 5
var reps: Int = 20
/// : = ( ), = ( )
var contractPattern: HapticPattern = .long
var relaxPattern: HapticPattern = .basic
/// (0 = , 1~10)
var effortScore: Int = 2
///
var healthWorkoutType: HealthWorkoutType = .other
init(
prepSeconds: Int = 3,
contractSeconds: Int = 10,
relaxSeconds: Int = 5,
reps: Int = 20,
contractPattern: HapticPattern = .long,
relaxPattern: HapticPattern = .basic,
effortScore: Int = 2,
healthWorkoutType: HealthWorkoutType = .other
) {
self.prepSeconds = prepSeconds
self.contractSeconds = contractSeconds
self.relaxSeconds = relaxSeconds
self.reps = reps
self.contractPattern = contractPattern
self.relaxPattern = relaxPattern
self.effortScore = effortScore
self.healthWorkoutType = healthWorkoutType
}
// JSON decodeIfPresent
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
prepSeconds = try container.decodeIfPresent(Int.self, forKey: .prepSeconds) ?? 3
contractSeconds = try container.decodeIfPresent(Int.self, forKey: .contractSeconds) ?? 10
relaxSeconds = try container.decodeIfPresent(Int.self, forKey: .relaxSeconds) ?? 5
reps = try container.decodeIfPresent(Int.self, forKey: .reps) ?? 20
contractPattern = HapticPattern(migrating: try container.decodeIfPresent(String.self, forKey: .contractPattern)) ?? .long
relaxPattern = HapticPattern(migrating: try container.decodeIfPresent(String.self, forKey: .relaxPattern)) ?? .basic
effortScore = try container.decodeIfPresent(Int.self, forKey: .effortScore) ?? 2
healthWorkoutType = try container.decodeIfPresent(HealthWorkoutType.self, forKey: .healthWorkoutType) ?? .other
}
var totalDuration: TimeInterval { Double(reps * (contractSeconds + relaxSeconds)) }
var summaryText: String {
String(localized: "수축 \(contractSeconds)초 · 이완 \(relaxSeconds)초 · \(reps)")
}
}
@MainActor
final class SettingsStore: ObservableObject {
static let shared = SettingsStore()
@Published var config: WorkoutConfig {
didSet {
save()
if config != oldValue { onConfigChange?(config) }
}
}
/// (iOS )
var onConfigChange: ((WorkoutConfig) -> Void)?
private static let key = "workout.config.v1"
private init() {
if let data = AppGroup.defaults.data(forKey: Self.key),
let saved = try? JSONDecoder().decode(WorkoutConfig.self, from: data) {
config = saved
} else {
config = WorkoutConfig()
}
}
private func save() {
if let data = try? JSONEncoder().encode(config) {
AppGroup.defaults.set(data, forKey: Self.key)
}
}
}