Files
coach-ios/ios/App/App/CoachWorkoutKit.swift
Sylvain Bettinelli 483ad09c20 fix CoachWorkoutKit : retire preview() (API absente du SDK), garde schedule()
L'API WorkoutScheduler.shared.preview(plan) n'existe pas (ou pas avec cette
signature) dans le SDK iOS de l'utilisateur — compile error 'Value of type
WorkoutScheduler has no...'.

On garde uniquement WorkoutScheduler.shared.schedule(plan, at: now), qui
est l'API stable depuis iOS 17.0. Effet : le workout devient immédiatement
disponible dans Apple Watch → Exercice → Workouts personnalisés.

Le 1er schedule déclenche automatiquement la dialog d'autorisation iOS
'Coach Hypnotruck souhaite ajouter des workouts à votre Apple Watch' si pas
encore accordée. Plus besoin du authorizationState query manuel.

Trade-off : pas de sheet de prévisualisation système avant ajout (l'user ne
voit pas la structure dans une UI native avant que le workout soit ajouté).
Mais c'est le bon compromis pour avoir un build qui marche. On pourra
réintroduire preview() plus tard si on bump le deployment target.
2026-05-08 09:49:16 +00:00

154 lines
5.5 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// CoachWorkoutKit.swift
// Plugin Capacitor natif iOS 17+ pour pousser des workouts structurés (warmup +
// intervals + cooldown) vers l'app Exercice de l'Apple Watch via WorkoutKit.
//
// Usage côté JS (dans la WebView) :
// await window.Capacitor.Plugins.CoachWorkoutKit.isAvailable()
// { available: bool, reason?: string }
// await window.Capacitor.Plugins.CoachWorkoutKit.sendInterval({
// activity: 'running' | 'cycling' | 'walking' | 'hiking',
// displayName: 'Coach séance 6×3min Z4',
// warmupMin: 10,
// work: { duration_min: 3, hr_zone: 4 },
// rest: { duration_min: 1.5, hr_zone: 2 },
// repeats: 6,
// cooldownMin: 10,
// })
// { sent: true } si la sheet a été présentée et user a tapé Ajouter
// reject avec error string sinon
//
// Sur iOS 17.4+ : utilise WorkoutScheduler.shared.preview(plan) qui ouvre
// la sheet système iOS 'Ajouter aux workouts'.
// Sur iOS 17.0-17.3 (rare) : fallback schedule() qui ajoute direct.
import Foundation
import Capacitor
#if canImport(WorkoutKit)
import WorkoutKit
import HealthKit
#endif
@objc(CoachWorkoutKitPlugin)
public class CoachWorkoutKitPlugin: CAPPlugin {
@objc func isAvailable(_ call: CAPPluginCall) {
if #available(iOS 17.0, *) {
call.resolve(["available": true, "min_ios": "17.0"])
} else {
call.resolve(["available": false, "reason": "iOS 17+ required for WorkoutKit"])
}
}
@objc func sendInterval(_ call: CAPPluginCall) {
guard #available(iOS 17.0, *) else {
call.reject("WorkoutKit requires iOS 17.0 or later")
return
}
let activityStr = call.getString("activity") ?? "running"
let displayName = call.getString("displayName") ?? "Coach séance"
let warmupMin = call.getDouble("warmupMin") ?? 10
let cooldownMin = call.getDouble("cooldownMin") ?? 10
let repeats = max(1, min(50, call.getInt("repeats") ?? 6))
let workDict = call.getObject("work") ?? [:]
let workMin = workDict["duration_min"] as? Double ?? 3
let workHrZone = workDict["hr_zone"] as? Int
let restDict = call.getObject("rest") ?? [:]
let restMin = restDict["duration_min"] as? Double ?? 1.5
let restHrZone = restDict["hr_zone"] as? Int
let activity = workoutActivity(from: activityStr)
Task {
do {
try await sendCustomWorkout(
activity: activity,
displayName: displayName,
warmupMin: warmupMin,
workMin: workMin,
workHrZone: workHrZone,
restMin: restMin,
restHrZone: restHrZone,
repeats: repeats,
cooldownMin: cooldownMin
)
call.resolve(["sent": true])
} catch {
call.reject("Failed to send workout: \(error.localizedDescription)")
}
}
}
private func workoutActivity(from str: String) -> HKWorkoutActivityType {
switch str.lowercased() {
case "cycling": return .cycling
case "walking": return .walking
case "hiking": return .hiking
case "running": return .running
default: return .running
}
}
@available(iOS 17.0, *)
private func sendCustomWorkout(
activity: HKWorkoutActivityType,
displayName: String,
warmupMin: Double,
workMin: Double,
workHrZone: Int?,
restMin: Double,
restHrZone: Int?,
repeats: Int,
cooldownMin: Double
) async throws {
// 1. Warmup un WorkoutStep avec un goal time
let warmupStep = WorkoutStep(goal: .time(warmupMin * 60, .seconds))
// 2. Work step wrapped dans un IntervalStep avec purpose .work
var workWorkoutStep = WorkoutStep(goal: .time(workMin * 60, .seconds))
if let zone = workHrZone {
workWorkoutStep.alert = HeartRateZoneAlert(zone: zone)
}
let workInterval = IntervalStep(.work, step: workWorkoutStep)
// 3. Rest step IntervalStep purpose .recovery
var restWorkoutStep = WorkoutStep(goal: .time(restMin * 60, .seconds))
if let zone = restHrZone {
restWorkoutStep.alert = HeartRateZoneAlert(zone: zone)
}
let restInterval = IntervalStep(.recovery, step: restWorkoutStep)
// 4. Block répété N fois
let block = IntervalBlock(steps: [workInterval, restInterval], iterations: repeats)
// 5. Cooldown
let cooldownStep = WorkoutStep(goal: .time(cooldownMin * 60, .seconds))
// 6. CustomWorkout
let custom = CustomWorkout(
activity: activity,
location: .outdoor,
displayName: displayName,
warmup: warmupStep,
blocks: [block],
cooldown: cooldownStep
)
// 7. Plan
let plan = WorkoutPlan(.custom(custom))
// 8. Schedule pour "maintenant" l'API la plus stable iOS 17.0+.
// Le workout devient immédiatement disponible dans Apple Watch
// Exercice Workouts personnalisés. Le 1er schedule déclenche la
// demande d'autorisation native si besoin.
let now = Calendar.current.dateComponents(
[.year, .month, .day, .hour, .minute],
from: Date()
)
_ = try await WorkoutScheduler.shared.schedule(plan, at: now)
}
}