mycode/myApp/Keging/Shared/WorkoutSettings.swift
songyc macbook 893545e7be feat(keging): 애플 건강 연동 — 완주 세트 운동 기록 + 운동 강도(1~10) 자동 저장
- 완주한 세트만 애플 건강에 '기타' 운동으로 자동 기록 (중단=미기록 규칙 일관)
- 폰=사후 HKWorkoutBuilder, 워치=라이브 HKWorkoutSession(심박 포함·손목 내림 유지 겸용,
  실패 시 확장 런타임 세션 폴백)
- 운동 강도: 설정 → 애플 건강 → 자동 기록(기본 2·쉬움, 안 함 가능) — relateWorkoutEffortSample,
  워치 11+/iOS 18, 워치 10은 운동만 기록
- 쓰기 전용 권한(첫 시작 때 요청·거부 시 조용히 스킵), 건강 문구 3언어, 새 UI 문자열 11키 번역
- WorkoutConfig 전 필드 decodeIfPresent 디코더 (필드 추가에도 기존 설정 보존)
- 검증: Debug·Release·워치 빌드 그린, 권한 시트·설정 ko/ja 화면 QA, 카탈로그 missing·stale 0
  (건강 실제 저장은 권한 시트 조작 불가로 실기기 확인 필요)

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

112 lines
3.6 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
/// / . ·
nonisolated enum HapticPattern: String, CaseIterable, Identifiable, Codable {
case once, twice, thrice, heartbeat
var id: String { rawValue }
var label: String {
switch self {
case .once: String(localized: "한 번")
case .twice: String(localized: "두 번")
case .thrice: String(localized: "세 번")
case .heartbeat: String(localized: "심장 박동")
}
}
/// () 0
var beatOffsets: [TimeInterval] {
switch self {
case .once: [0]
case .twice: [0, 0.45]
case .thrice: [0, 0.45, 0.9]
case .heartbeat: [0, 0.3, 1.0, 1.3]
}
}
}
/// = ( ) ×
nonisolated struct WorkoutConfig: Codable, Equatable {
var contractSeconds: Int = 10
var relaxSeconds: Int = 5
var reps: Int = 20
var contractPattern: HapticPattern = .twice
var relaxPattern: HapticPattern = .once
/// (0 = , 1~10)
var effortScore: Int = 2
init(
contractSeconds: Int = 10,
relaxSeconds: Int = 5,
reps: Int = 20,
contractPattern: HapticPattern = .twice,
relaxPattern: HapticPattern = .once,
effortScore: Int = 2
) {
self.contractSeconds = contractSeconds
self.relaxSeconds = relaxSeconds
self.reps = reps
self.contractPattern = contractPattern
self.relaxPattern = relaxPattern
self.effortScore = effortScore
}
// JSON decodeIfPresent
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
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 = try container.decodeIfPresent(HapticPattern.self, forKey: .contractPattern) ?? .twice
relaxPattern = try container.decodeIfPresent(HapticPattern.self, forKey: .relaxPattern) ?? .once
effortScore = try container.decodeIfPresent(Int.self, forKey: .effortScore) ?? 2
}
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)
}
}
}