mycode/myApp/Keging/Shared/WorkoutSettings.swift
songyc macbook 06d13b2f3d feat(keging): 준비 시간(0~10초, 기본 3초) — 시작 버튼 후 카운트다운 뒤 첫 수축
- KegelPhase에 prepare 국면 추가(무진동 — 첫 수축 진동이 시작 신호), 준비 0초면 즉시 수축
- 세팅에 '준비' 스테퍼(0~10초), 워치 자동 동기화, 구버전 설정 decodeIfPresent 보존
- 진행 화면: 준비=회그린 원이 작게 시작해 천천히 부풀며 첫 수축으로 연결(기존 그래픽 문법 유지),
  진행 링 0·카운터 색 연동. 워치·Live Activity도 준비 국면 색/표시 대응
- 기록·건강의 시작 시각은 준비를 뺀 첫 수축 기준. Live Activity는 sync 단일 진입점으로 정리
- 도움말·세팅 푸터 문구 갱신, 신규 문자열 번역(en/ja)·stale 정리 — 전 카탈로그 0/0
- 검증: Debug·Release·워치 빌드 그린, 준비→수축 전환·세팅 화면 QA

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

154 lines
5.4 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 short, doubleTap, rattle, long
var id: String { rawValue }
var label: String {
switch self {
case .short: String(localized: "톡 (짧게)")
case .doubleTap: String(localized: "톡톡")
case .rattle: String(localized: "드르륵")
case .long: String(localized: "지잉 (길게)")
}
}
/// () 1 0.4
/// 0.35 , 0.5
var beatOffsets: [TimeInterval] {
switch self {
case .short: [0]
case .doubleTap: [0, 0.5]
case .rattle: [0, 0.5, 1.0]
case .long: [0, 0.3, 0.6]
}
}
/// 1.0 ( )
init?(migrating raw: String?) {
guard let raw else { return nil }
if let direct = HapticPattern(rawValue: raw) {
self = direct
return
}
switch raw {
case "once": self = .short
case "twice": self = .doubleTap
case "thrice", "heartbeat": self = .rattle
default: return nil
}
}
}
/// (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 = .short
/// (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 = .short,
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)) ?? .short
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)
}
}
}