import SwiftUI import HealthKit struct ContentView: View { @EnvironmentObject private var workout: WorkoutManager var body: some View { Group { if workout.isRunning { LiveWorkoutView() } else { ActivityPickerView() } } .task { // Déclenchement auto du mode test (validation simulateur) : // SIMCTL_CHILD_AUTOSIM=1 xcrun simctl launch … if ProcessInfo.processInfo.environment["AUTOSIM"] == "1" { workout.startSimulation() } } } } struct ActivityPickerView: View { @EnvironmentObject private var workout: WorkoutManager @State private var showRoutine = false @State private var showVma = false private struct Activity: Identifiable { let id = UUID() let label: String let symbol: String let type: HKWorkoutActivityType let location: HKWorkoutSessionLocationType } private let activities: [Activity] = [ .init(label: "Course", symbol: "figure.run", type: .running, location: .outdoor), .init(label: "Vélo", symbol: "figure.outdoor.cycle", type: .cycling, location: .outdoor), .init(label: "Marche", symbol: "figure.walk", type: .walking, location: .outdoor), .init(label: "Renfo", symbol: "dumbbell", type: .traditionalStrengthTraining, location: .indoor) ] var body: some View { ScrollView { VStack(spacing: 8) { ForEach(activities) { activity in Button { Task { await workout.startWorkout(activityType: activity.type, locationType: activity.location) } } label: { Label(activity.label, systemImage: activity.symbol) .frame(maxWidth: .infinity, alignment: .leading) } } Button { showRoutine = true } label: { Label("Routine du jour", systemImage: "figure.cooldown") .frame(maxWidth: .infinity, alignment: .leading) } .tint(.cyan) Button { showVma = true } label: { Label("Test VMA", systemImage: "speedometer") .frame(maxWidth: .infinity, alignment: .leading) } .tint(.orange) Button { workout.startSimulation() } label: { Label("Test (FC simulée)", systemImage: "waveform.path.ecg") .frame(maxWidth: .infinity, alignment: .leading) } .tint(.gray) if let message = workout.statusMessage { Text(message) .font(.footnote) .foregroundStyle(.secondary) } } .padding(.horizontal, 4) } .navigationTitle("Coach") .sheet(isPresented: $showRoutine) { RoutineView() } .sheet(isPresented: $showVma) { VmaTestView() } } } struct LiveWorkoutView: View { @EnvironmentObject private var workout: WorkoutManager @EnvironmentObject private var connectivity: ConnectivityManager var body: some View { ScrollView { VStack(alignment: .leading, spacing: 10) { HeartRateMetric(bpm: Int(workout.heartRate), zones: connectivity.zones) Metric(label: "Calories", value: "\(Int(workout.activeEnergyKcal))", unit: "kcal", color: .orange) Metric(label: "Distance", value: String(format: "%.2f", workout.distanceMeters / 1000), unit: "km", color: .blue) // km/h plutôt que l'allure min/km : l'app Exercice d'Apple ne // propose la vitesse qu'à vélo, jamais en course. Metric(label: "Vitesse", value: workout.speedKmh > 0 ? String(format: "%.1f", workout.speedKmh) : "—", unit: "km/h", color: .cyan) Metric(label: "Durée", value: Self.timeString(workout.elapsedSec), unit: "", color: .green) HStack(spacing: 4) { Image(systemName: connectivity.isReachable ? "iphone.radiowaves.left.and.right" : "iphone.slash") .foregroundStyle(connectivity.isReachable ? .green : .secondary) Text(connectivity.isReachable ? "iPhone connecté" : "iPhone hors de portée") .font(.caption2) .foregroundStyle(.secondary) } // Pause/reprise : la Live Activity de l'iPhone pilote les mêmes // appels, l'état affiché ici doit donc suivre `isPaused` et non // un état local au bouton. Button { workout.isPaused ? workout.resume() : workout.pause() } label: { Label(workout.isPaused ? "Reprendre" : "Pause", systemImage: workout.isPaused ? "play.fill" : "pause.fill") .frame(maxWidth: .infinity) } .tint(workout.isPaused ? .green : .yellow) Button(role: .destructive) { workout.end() } label: { Label("Terminer", systemImage: "stop.fill") .frame(maxWidth: .infinity) } } .padding(.horizontal, 4) } } private static func timeString(_ seconds: Double) -> String { let total = Int(seconds) let h = total / 3600 let m = (total % 3600) / 60 let s = total % 60 return h > 0 ? String(format: "%d:%02d:%02d", h, m, s) : String(format: "%02d:%02d", m, s) } } /// FC de la séance, accompagnée de sa zone quand les zones sont connues. /// /// ⚠️ La couleur ne porte jamais l'information seule : « Z3 » est écrit à côté. /// Une pastille nue serait illisible pour un daltonien, et le contraste d'un /// écran de montre en plein soleil ne se prête pas aux nuances. private struct HeartRateMetric: View { let bpm: Int let zones: HeartRateZones? private var zone: HeartRateZone? { guard bpm > 0 else { return nil } return zones?.zone(for: bpm) } var body: some View { VStack(alignment: .leading, spacing: 0) { HStack(spacing: 6) { Text("FC") .font(.caption2) .foregroundStyle(.secondary) if let zone { Text(zone.label) .font(.caption2.weight(.bold)) .padding(.horizontal, 6) .padding(.vertical, 1) .background(zone.color.opacity(0.25), in: Capsule()) .foregroundStyle(zone.color) } } HStack(alignment: .firstTextBaseline, spacing: 2) { Text("\(bpm)") .font(.system(size: 30, weight: .semibold, design: .rounded)) // La valeur prend la couleur de la zone : d'un coup d'œil, // sans lire, on sait si l'on est trop haut. .foregroundStyle(zone?.color ?? .red) .contentTransition(.numericText()) Text("bpm") .font(.caption) .foregroundStyle(.secondary) } if let zone, let bounds = zones?[zone], bounds.count == 2 { Text("\(zone.name) · \(bounds[0])–\(bounds[1])") .font(.caption2) .foregroundStyle(.secondary) } } .accessibilityElement(children: .ignore) .accessibilityLabel("Fréquence cardiaque") .accessibilityValue(zone.map { "\(bpm) battements par minute, zone \($0.label), \($0.name)" } ?? "\(bpm) battements par minute") } } private struct Metric: View { let label: String let value: String let unit: String let color: Color var body: some View { VStack(alignment: .leading, spacing: 0) { Text(label) .font(.caption2) .foregroundStyle(.secondary) HStack(alignment: .firstTextBaseline, spacing: 2) { Text(value) .font(.system(size: 30, weight: .semibold, design: .rounded)) .foregroundStyle(color) if !unit.isEmpty { Text(unit) .font(.caption) .foregroundStyle(.secondary) } } } } } #Preview { ContentView() .environmentObject(WorkoutManager.shared) .environmentObject(ConnectivityManager.shared) }