Logger unified (capturable via simctl) côté watch (envoi) et iPhone (réception) pour valider le pont WCSession. Hook AUTOSIM (env var) démarre le mode test au lancement, pour validation automatisée sur simulateur. Pont prouvé E2E sur sim pair iPhone+Watch : HR identiques watch->iPhone. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
152 lines
5.2 KiB
Swift
152 lines
5.2 KiB
Swift
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
|
|
|
|
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 {
|
|
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")
|
|
}
|
|
}
|
|
|
|
struct LiveWorkoutView: View {
|
|
@EnvironmentObject private var workout: WorkoutManager
|
|
@EnvironmentObject private var connectivity: ConnectivityManager
|
|
|
|
var body: some View {
|
|
ScrollView {
|
|
VStack(alignment: .leading, spacing: 10) {
|
|
Metric(label: "FC", value: "\(Int(workout.heartRate))", unit: "bpm", color: .red)
|
|
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)
|
|
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)
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|