mycode/myApp/Keging/KegingWatch Watch App/WatchSettingsView.swift
songyc macbook 42850683c0 fix(keging): 워치 기본·톡톡 진동 알림급 강화 + 세팅 크라운 사용성 재설계 (8차)
- 기본/톡톡의 .start가 실기기에서 너무 약함 → 가장 강한 알림급 .notification으로
  교체 (기본 1방, 톡톡 2방 0.5초 간격 — 알림 진동이 길어 간격 넉넉히)
- 세팅 화면: 스테퍼가 크라운을 뺏어 스크롤과 값 변경이 충돌 → 목록에서 크라운은
  스크롤 전용으로, 준비/수축/이완/횟수는 행을 눌러 전용 화면(CrownValueEditor —
  큰 민트 값 + digitalCrownRotation)에서 크라운으로 조절. 진동·건강은 눌러서 선택
- DEBUG -openEditor(워치, 화면 QA용) 추가, '크라운을 돌려 조절' en/ja 번역
- 시뮬 검증: 세팅 목록·크라운 편집 화면(46mm), 워치 빌드 그린·경고 0·l10n 0

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPmp29DQHY6Ahfss5SRopT
2026-09-03 03:27:39 +09:00

168 lines
6.5 KiB
Swift

//
// WatchSettingsView.swift
// KegingWatch Watch App
//
// .
// ( ), .
//
// (8 ):
// ,
// (CrownValueEditor). · .
// .
//
import SwiftUI
struct WatchSettingsView: View {
@EnvironmentObject private var settings: SettingsStore
#if DEBUG
/// QA `-openEditor` ( )
@State private var qaEditorShown = false
#endif
var body: some View {
NavigationStack {
List {
Section {
valueRow(Text("준비"), value: Text("\(settings.config.prepSeconds)")) {
CrownValueEditor(title: Text("준비"), range: 0...10, value: $settings.config.prepSeconds) {
Text("\($0)")
}
}
valueRow(Text("수축"), value: Text("\(settings.config.contractSeconds)")) {
CrownValueEditor(title: Text("수축"), range: 1...60, value: $settings.config.contractSeconds) {
Text("\($0)")
}
}
valueRow(Text("이완"), value: Text("\(settings.config.relaxSeconds)")) {
CrownValueEditor(title: Text("이완"), range: 1...60, value: $settings.config.relaxSeconds) {
Text("\($0)")
}
}
valueRow(Text("횟수"), value: Text("\(settings.config.reps)")) {
CrownValueEditor(title: Text("횟수"), range: 1...100, value: $settings.config.reps) {
Text("\($0)")
}
}
} header: {
Text("세트")
}
Section {
Picker(selection: $settings.config.contractPattern) {
ForEach(HapticPattern.allCases) { Text($0.label).tag($0) }
} label: {
Text("수축")
}
Picker(selection: $settings.config.relaxPattern) {
ForEach(HapticPattern.allCases) { Text($0.label).tag($0) }
} label: {
Text("이완")
}
} header: {
Text("진동")
} footer: {
Text("고르면 바로 진동이 울려요. 세팅은 아이폰과 따로 저장돼요.")
}
Section {
Picker(selection: $settings.config.healthWorkoutType) {
ForEach(HealthWorkoutType.allCases) { Text($0.label).tag($0) }
} label: {
Text("운동 유형")
}
Picker(selection: $settings.config.effortScore) {
Text("안 함").tag(0)
ForEach(1...10, id: \.self) { score in
Text(verbatim: "\(score)").tag(score)
}
} label: {
Text("운동 강도")
}
} header: {
Text("애플 건강")
}
}
.navigationTitle("세팅")
.onChange(of: settings.config.contractPattern) { _, newValue in
WatchHaptics.play(newValue)
}
.onChange(of: settings.config.relaxPattern) { _, newValue in
WatchHaptics.play(newValue)
}
#if DEBUG
.navigationDestination(isPresented: $qaEditorShown) {
CrownValueEditor(title: Text("준비"), range: 0...10, value: $settings.config.prepSeconds) {
Text("\($0)")
}
}
.onAppear {
if CommandLine.arguments.contains("-openEditor") { qaEditorShown = true }
}
#endif
}
}
/// +
private func valueRow<Editor: View>(
_ title: Text, value: Text, @ViewBuilder editor: @escaping () -> Editor
) -> some View {
NavigationLink {
editor()
} label: {
HStack {
title
Spacer()
value
.foregroundStyle(.secondary)
.monospacedDigit()
}
}
}
}
///
struct CrownValueEditor: View {
let title: Text
let range: ClosedRange<Int>
@Binding var value: Int
let format: (Int) -> Text
@State private var crownValue = 0.0
@FocusState private var focused: Bool
var body: some View {
VStack(spacing: 10) {
title
.font(.headline)
.foregroundStyle(.secondary)
format(value)
.font(.system(size: 40, weight: .bold))
.monospacedDigit()
.foregroundStyle(AppTheme.green)
.contentTransition(.numericText())
.animation(.snappy, value: value)
Text("크라운을 돌려 조절")
.font(.caption2)
.foregroundStyle(.secondary)
}
.focusable(true)
.focused($focused)
.digitalCrownRotation(
$crownValue,
from: Double(range.lowerBound),
through: Double(range.upperBound),
by: 1,
sensitivity: .medium,
isContinuous: false,
isHapticFeedbackEnabled: true
)
.onChange(of: crownValue) { _, newValue in
let rounded = min(max(Int(newValue.rounded()), range.lowerBound), range.upperBound)
if rounded != value { value = rounded }
}
.onAppear {
crownValue = Double(value)
focused = true
}
}
}