mycode/myApp/Keging/Shared/WorkoutSettings.swift
songyc macbook 0d058c7734 fix(keging): 진동 패턴 4종 재설계 — 횟수 세기 대신 첫 순간의 질감으로 구분
- 톡(짧게)/톡톡/드르륵/지잉(길게, 0.3s 겹침으로 끊기지 않는 긴 진동) — 사용자 피드백
  '한 번·두 번 방식은 아니고 간격도 길다' 반영, 연타 간격 0.5s로 단축
- 기본값: 수축=지잉(조이고 유지), 이완=톡(탁 풀기) — 첫 진동 즉시 구분
- 워치는 반복 재생 대신 패턴별 단일 시스템 햅틱(.start/.directionUp/.retry/.notification)
  — 워치 연타 뭉개짐·긴 간격 문제 자체를 제거
- 구버전 저장값(once/twice/thrice/heartbeat) 자동 이관, 라벨 3언어 번역·stale 정리
- 검증: Debug·Release·워치 빌드 그린, 세팅 화면 QA, 카탈로그 missing·stale 0

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

149 lines
5.2 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 {
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(
contractSeconds: Int = 10,
relaxSeconds: Int = 5,
reps: Int = 20,
contractPattern: HapticPattern = .long,
relaxPattern: HapticPattern = .short,
effortScore: Int = 2,
healthWorkoutType: HealthWorkoutType = .other
) {
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)
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)
}
}
}