mycode/myApp/Keging/IOS/ContentView.swift
songyc macbook 43448b35f3 feat(keging): 1.0 전 기능 구현 — iOS 타이머·진동 패턴·통계·다이나믹 아일랜드·단축어·워치 앱
- 세트 엔진(수축→이완×횟수, 중단=미기록) iOS·워치 공용, 완주만 records.json 저장
- 첫 화면 시작 버튼 + 세팅(시간·횟수·진동 패턴)/설정(테마) 시트, 아래 스와이프 통계
- 통계: 하루(시간대별)/주간/월간 꺾은선, 달력 기준↔롤링(지난 7일·30일) 전환
- Live Activity(수축/이완·n/총·카운트다운), 백그라운드 무음 오디오로 타이머·진동 유지
- 단축어 '케겔 운동 시작', 워치 앱(즉시 시작·워치 전용 진동·physical-therapy 세션·기록 폰 병합)
- 하루 다님 팔레트 계승(라이트/다크/시스템), 앱 아이콘 SVG→PNG 등록
- 검증: Debug·Release·워치 빌드 그린, 시뮬 QA 10장(26·18.5·워치 46/40mm), 완주·기록 경로 확인

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

172 lines
5.7 KiB
Swift

//
// ContentView.swift
// Keging
//
// + · . .
//
import SwiftUI
struct ContentView: View {
@EnvironmentObject private var engine: KegelEngine
@EnvironmentObject private var settings: SettingsStore
@State private var showTimerSettings = false
@State private var showAppSettings = false
@State private var showStats = false
var body: some View {
ZStack {
AppTheme.background.ignoresSafeArea()
mainScreen
if showStats {
StatsView(onClose: closeStats)
.transition(.move(edge: .top))
.zIndex(2)
}
}
.sheet(isPresented: $showTimerSettings) { TimerSettingsView() }
.sheet(isPresented: $showAppSettings) { AppSettingsView() }
.fullScreenCover(isPresented: workoutPresented) { WorkoutView() }
.onReceive(NotificationCenter.default.publisher(for: .startWorkoutRequested)) { _ in
startFromShortcut()
}
.onAppear {
if AppLaunchState.shared.startRequested { startFromShortcut() }
#if DEBUG
if CommandLine.arguments.contains("-openStats") { showStats = true }
if CommandLine.arguments.contains("-autoStart") { WorkoutCoordinator.shared.startWorkout() }
#endif
}
}
// MARK:
private var mainScreen: some View {
VStack(spacing: 0) {
HStack(alignment: .top) {
cornerButton(symbol: "gearshape.fill", label: "설정") { showAppSettings = true }
Spacer()
Button {
openStats()
} label: {
VStack(spacing: 2) {
Capsule().fill(.secondary.opacity(0.5)).frame(width: 36, height: 5)
Image(systemName: "chevron.compact.down")
.font(.body)
.foregroundStyle(.secondary)
}
.padding(.top, 10)
}
.buttonStyle(.plain)
.accessibilityLabel("통계 보기")
Spacer()
cornerButton(symbol: "slider.horizontal.3", label: "운동 세팅") { showTimerSettings = true }
}
.padding(.horizontal, 20)
.padding(.top, 8)
Spacer()
Button {
WorkoutCoordinator.shared.startWorkout()
} label: {
startButtonLabel
}
.buttonStyle(.plain)
.accessibilityLabel("케겔 운동 시작")
Button {
showTimerSettings = true
} label: {
Text(settings.config.summaryText)
.font(.subheadline)
.foregroundStyle(.secondary)
}
.buttonStyle(.plain)
.padding(.top, 30)
.accessibilityLabel("운동 세팅 열기 — 현재 \(settings.config.summaryText)")
Spacer()
Spacer()
}
.contentShape(Rectangle())
.gesture(
DragGesture(minimumDistance: 25)
.onEnded { value in
if value.translation.height > 60, abs(value.translation.width) < 100 {
openStats()
}
}
)
}
private var startButtonLabel: some View {
ZStack {
Circle()
.fill(
RadialGradient(
colors: [AppTheme.green.opacity(0.22), .clear],
center: .center, startRadius: 100, endRadius: 165
)
)
.frame(width: 330, height: 330)
Circle()
.fill(
LinearGradient(
colors: [AppTheme.green, AppTheme.green.opacity(0.82)],
startPoint: .top, endPoint: .bottom
)
)
.frame(width: 236, height: 236)
.shadow(color: AppTheme.green.opacity(0.35), radius: 24, x: 0, y: 10)
VStack(spacing: 8) {
Image(systemName: "play.fill").font(.system(size: 40, weight: .bold))
Text("시작").font(.title.bold())
}
.foregroundStyle(startButtonForeground)
}
.frame(width: 330, height: 330)
}
///
private var startButtonForeground: Color {
Color(light: .white, dark: Color(hex: "#0F2018"))
}
private func cornerButton(symbol: String, label: String, action: @escaping () -> Void) -> some View {
Button(action: action) {
Image(systemName: symbol)
.font(.title3)
.foregroundStyle(.secondary)
.frame(width: 44, height: 44)
.background(AppTheme.surface, in: Circle())
}
.buttonStyle(.plain)
.accessibilityLabel(label)
}
// MARK: ·
private func openStats() {
withAnimation(.spring(duration: 0.35)) { showStats = true }
}
private func closeStats() {
withAnimation(.spring(duration: 0.35)) { showStats = false }
}
private var workoutPresented: Binding<Bool> {
Binding(
get: { engine.state != .idle },
set: { presented in if !presented { engine.acknowledgeFinish() } }
)
}
private func startFromShortcut() {
AppLaunchState.shared.startRequested = false
showStats = false
WorkoutCoordinator.shared.startWorkout()
}
}