import SwiftUI struct WatchCardiacMirrorView: View { enum Stage { case intro, measuring, ready, mirroring, completed } @EnvironmentObject private var model: WatchAppModel @State private var stage: Stage = .intro @State private var initialBPM: Double? @State private var errorMessage: String? @State private var mirrorStartedAt: Date? var body: some View { ScrollView { VStack(spacing: 10) { switch stage { case .intro: Image(systemName: "heart.circle.fill").font(.system(size: 40)).foregroundStyle(.pink) Text("Miroir cardiaque").font(.headline) Text(model.connectivity.isReachable ? "Mode connecté : la Watch mesure, l’iPhone peut reconstruire le rythme visuel et haptique." : "Mode Estimation Watch : mesure initiale, arrêt du capteur, puis rythme reconstruit et ralenti.") .font(.caption2).foregroundStyle(WatchTheme.secondary).multilineTextAlignment(.center) Button("Mesurer le rythme") { startMeasurement() }.buttonStyle(.borderedProminent) case .measuring: Text(model.measurement.currentBPM.map { "\(Int($0.rounded())) BPM" } ?? "— BPM").font(.title2.bold()) measurementProgress Text("Aucune vibration pendant la mesure").font(.caption2).foregroundStyle(.orange) Button("Annuler", role: .cancel) { model.measurement.cancel(); stage = .intro } case .ready: Text(initialBPM.map { "Repère initial \(Int($0.rounded())) BPM" } ?? "Repère indisponible").font(.headline) Text("Le rythme suivant est une estimation reconstruite, pas chaque battement biologique.") .font(.caption2).foregroundStyle(WatchTheme.secondary).multilineTextAlignment(.center) if model.connectivity.isReachable { Label("iPhone connecté", systemImage: "iphone.and.arrow.forward").font(.caption).foregroundStyle(WatchTheme.mint) Button("Terminer") { stage = .completed } } else { Button("Lancer le miroir") { startMirror() }.buttonStyle(.borderedProminent) } case .mirroring: estimatedPulse ProgressView(value: model.haptics.elapsedSeconds, total: 120) Text("Ralentissement progressif · −1,5 BPM/min").font(.caption2).foregroundStyle(WatchTheme.secondary) Button("Arrêter", role: .cancel) { model.haptics.cancel(); stage = .completed } case .completed: Image(systemName: "checkmark.circle.fill").font(.title).foregroundStyle(WatchTheme.mint) Text("Expérience terminée").font(.headline) Button("Recommencer") { stage = .intro; model.measurement.resetAfterResult() } } if let errorMessage { Text(errorMessage).font(.caption2).foregroundStyle(.orange) } } .padding(.horizontal, 4) } .navigationTitle("Miroir") .haptikaScreen() .onChange(of: model.measurement.state) { _, state in if case let .completed(summary) = state { initialBPM = summary.averageBPM stage = .ready } } .onChange(of: model.haptics.isRunning) { wasRunning, isRunning in if stage == .mirroring, wasRunning, !isRunning { stage = .completed } } .onDisappear { if stage == .measuring { model.measurement.cancel() } if stage == .mirroring { model.haptics.cancel() } } } @ViewBuilder private var measurementProgress: some View { switch model.measurement.state { case let .waitingForFirstSample(seconds): ProgressView(); Text("Signal · \(seconds) s").font(.caption2) case let .collecting(progress): ProgressView(value: progress) case .finishing: ProgressView("Analyse…") default: EmptyView() } } private var estimatedPulse: some View { TimelineView(.animation(minimumInterval: 1 / 20)) { context in let elapsed = mirrorStartedAt.map { context.date.timeIntervalSince($0) } ?? 0 let bpm = max(35, (initialBPM ?? 60) - 1.5 * elapsed / 60) let phase = (elapsed * bpm / 60).truncatingRemainder(dividingBy: 1) let scale = 0.78 + 0.22 * max(0, 1 - phase * 4) VStack { Image(systemName: "heart.fill").font(.system(size: 62)).foregroundStyle(.pink).scaleEffect(scale) Text("≈ \(Int(bpm.rounded())) BPM").font(.headline.monospacedDigit()) } } } private func startMeasurement() { stage = .measuring model.measurement.resetAfterResult() Task { do { try await model.measurement.requestAuthorization() try await model.measurement.start() } catch { errorMessage = error.localizedDescription } } } private func startMirror() { guard let initialBPM else { return } mirrorStartedAt = Date() stage = .mirroring Task { do { try await model.haptics.startCardiacMirror(initialBPM: initialBPM) } catch { errorMessage = error.localizedDescription; stage = .ready } } } }