feat(widgets): widgets natifs iPhone (Séance du jour + Score de forme)
Pont WebView→App Group→WidgetKit : - CoachWidgetSnapshot.swift : modèle partagé App↔extension (App Group group.ch.hypnotruck.coach) - CoachWidgetBridge.swift : plugin Capacitor setSnapshot/clear + WidgetCenter.reloadAllTimelines - CoachWidgets.swift : 2 widgets (TimelineProvider + vues SwiftUI, small/medium) - bundle + enregistrement plugin + entitlements App Group (App + CoachLiveActivity) ⚠️ Reste câblage Mac (App Group portail + capability + Target Membership) : docs/widgets-runbook-mac.md. Hook web côté coach_sportif. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
56
ios/App/App/CoachWidgetSnapshot.swift
Normal file
56
ios/App/App/CoachWidgetSnapshot.swift
Normal file
@@ -0,0 +1,56 @@
|
||||
// CoachWidgetSnapshot.swift
|
||||
// Modèle de données PARTAGÉ entre l'app (cible App) et l'extension widget
|
||||
// (cible CoachLiveActivity). L'app écrit le snapshot du jour dans l'App Group
|
||||
// via CoachWidgetBridge ; les widgets d'écran d'accueil le lisent.
|
||||
//
|
||||
// ⚠️ Ce fichier doit appartenir aux DEUX cibles (App + CoachLiveActivity) —
|
||||
// cf. docs/widgets-runbook-mac.md (étape « Target Membership »).
|
||||
//
|
||||
// App Group requis : group.ch.hypnotruck.coach (à créer sur le portail Apple
|
||||
// et activer comme capability sur les deux cibles).
|
||||
import Foundation
|
||||
|
||||
enum CoachWidgetStore {
|
||||
/// Identifiant App Group partagé app ↔ extension. Doit correspondre
|
||||
/// EXACTEMENT à la capability "App Groups" des deux cibles.
|
||||
static let appGroup = "group.ch.hypnotruck.coach"
|
||||
static let snapshotKey = "coach_widget_snapshot"
|
||||
|
||||
static var defaults: UserDefaults? { UserDefaults(suiteName: appGroup) }
|
||||
|
||||
static func save(_ snapshot: CoachWidgetSnapshot) {
|
||||
guard let d = defaults, let data = try? JSONEncoder().encode(snapshot) else { return }
|
||||
d.set(data, forKey: snapshotKey)
|
||||
}
|
||||
|
||||
static func load() -> CoachWidgetSnapshot? {
|
||||
guard let d = defaults, let data = d.data(forKey: snapshotKey) else { return nil }
|
||||
return try? JSONDecoder().decode(CoachWidgetSnapshot.self, from: data)
|
||||
}
|
||||
|
||||
static func clear() {
|
||||
defaults?.removeObject(forKey: snapshotKey)
|
||||
}
|
||||
}
|
||||
|
||||
/// Instantané des données affichées par les widgets. Volontairement minimal :
|
||||
/// le widget n'a pas d'accès réseau, tout vient de ce snapshot poussé par l'app.
|
||||
struct CoachWidgetSnapshot: Codable {
|
||||
var updatedAt: Date
|
||||
var today: TodaySession?
|
||||
var forme: FormeScore?
|
||||
|
||||
/// Séance planifiée du jour (nil = jour OFF / pas de séance).
|
||||
struct TodaySession: Codable {
|
||||
var sport: String // running, cycling, strength, mobility, hiking, rest…
|
||||
var title: String // "Sortie longue", "Renfo bas du corps"…
|
||||
var subtitle: String? // objectif court : "1h30 · Z2", "4×12"…
|
||||
var done: Bool // séance déjà réalisée aujourd'hui
|
||||
}
|
||||
|
||||
/// Score de forme / récupération (nil = pas encore de score).
|
||||
struct FormeScore: Codable {
|
||||
var score: Int? // 0–100
|
||||
var label: String? // "Prêt", "Récup", "Fatigue"…
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user