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.
154 lines
5.5 KiB
Swift
154 lines
5.5 KiB
Swift
// 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)
|
||
}
|
||
}
|