# Session Xcode — guide pas à pas **Écrit le 2026-08-03 pour Sylvain, à suivre sur le Mac mini.** Cette session livre **tout le natif accumulé depuis le TestFlight de mai** : login Apple/Google natif, watchOS live workout, widgets iPhone, complications Watch, et la routine quotidienne sur la montre. Compte ~1 h 30 la première fois. Les étapes 1 à 6 mènent à une app qui tourne sur ton iPhone ; les étapes 7 à 9 la publient sur TestFlight. **Tu peux t'arrêter après l'étape 6** et publier un autre jour. --- ## Avant de commencer — ce qui a changé La plupart des Swift sont déjà déclarés dans les bonnes targets (commits `caeac37`, `f30cc60`). **Quatre** ne le sont pas et doivent être ajoutés à la main — état vérifié dans `project.pbxproj` le 2026-08-11 : | Fichier | Target Membership à cocher | Étape | |---|---|---| | `App/CoachQuickLog.swift` | **App + CoachLiveActivity** | 4b | | `App/CoachQuickSync.swift` | **App** seule | 4b | | `CoachLiveActivity/CoachQuickWidget.swift` | **CoachLiveActivity** seule | 4b | | `CoachWatchWidgets/CoachWatchComplications.swift` | **CoachWatchWidgets** (target à créer) | 5 | ⚠️ Les trois premiers sont arrivés le **2026-08-06**, après la rédaction de ce guide, qui annonçait « une seule exception » — c'était vrai le 3 août. Sans eux, le widget « Saisie des repas et boissons » ne se compile pas. Leur runbook détaillé est `docs/widgets-runbook-mac.md`. Déjà fait côté code, à ne pas refaire : `CoachQuickWidget()` est enregistré dans le `WidgetBundle` (`CoachLiveActivityBundle.swift`) et `CoachQuickSync.flush()` est appelé depuis `AppDelegate` (l. 57). | Ce que tu vas activer | Étape | |---|---| | Login Apple + Google natif | rien à faire, déjà câblé | | Live workout watchOS | rien à faire, déjà câblé | | Routine sur la Watch | rien à faire, déjà câblé | | Widgets iPhone (Séance du jour, Score de forme) | étapes 3-4 (App Group) | | Widget Saisie rapide (eau, café, photo, scan) | étape 4b (3 fichiers à ajouter) | | Complications sur le cadran Watch | étape 5 (facultative) | --- ## Étape 1 — Récupérer le code ⚠️ **Xcode doit être FERMÉ** (`Cmd+Q`, pas juste la fenêtre). Xcode réécrit le `project.pbxproj` depuis sa mémoire ; un `git pull` fenêtre ouverte nous a déjà bloqués deux fois. ```bash cd ~/coach-ios git pull npm install npx cap sync ios ``` **Vérification** — tu dois voir `f30cc60` ou plus récent : ```bash git log --oneline -1 ``` Si `npx cap sync` râle sur la version de Node : ```bash nvm use 22 # Capacitor 8 exige Node >= 22 ``` ## Étape 2 — Le token d'auth (à ne pas oublier) Le fichier `CoachAuth.swift` n'est pas versionné (il contient un secret). **Sans lui, le projet ne compile pas.** ```bash ls ios/App/App/CoachAuth.swift ``` - **Le fichier existe** → passe à l'étape 3. - **Il n'existe pas** → récupère le token puis crée-le : ```bash # le token est sur le VPS ssh ubuntu@83.228.246.229 'grep COACH_WEB_TOKEN ~/.config/infomaniak.env' # puis, en remplaçant par la valeur affichée : cat > ios/App/App/CoachAuth.swift <<'EOF' import Foundation enum CoachAuth { static let kCoachWebToken: String = "" } EOF ``` ## Étape 3 — App Group sur le portail Apple Nécessaire pour que les widgets lisent les données de l'app. À faire une seule fois, dans le navigateur. 1. [developer.apple.com](https://developer.apple.com) → **Certificates, IDs & Profiles** 2. **Identifiers** → menu déroulant en haut à droite → **App Groups** 3. Le groupe `group.ch.hypnotruck.coach` existe déjà ? → rien à faire. Sinon : **+** → *App Group* → Description `Coach Hypnotruck`, Identifier `group.ch.hypnotruck.coach` → **Continue** → **Register** ## Étape 4 — Activer la capability dans Xcode ```bash # Capacitor 8 utilise Swift Package Manager : il n'existe PAS de # .xcworkspace (celui-ci supposait CocoaPods). On ouvre le .xcodeproj. open ios/App/App.xcodeproj ``` Pour **chacune** de ces trois targets (colonne de gauche → projet **App** → onglet **TARGETS**) : | Target | Capability à ajouter | |---|---| | **App** | App Groups | | **CoachLiveActivity** | App Groups | | **CoachWatch** | App Groups | Pour chaque target : onglet **Signing & Capabilities** → **+ Capability** (en haut à gauche) → tape « App Groups » → double-clic → **coche** `group.ch.hypnotruck.coach` dans la liste qui apparaît. > Si le groupe n'apparaît pas dans la liste, clique sur le bouton **refresh** > sous la liste, ou re-connecte ton compte (Xcode → Settings → Accounts). ⚠️ **Dès que c'est fait, commit + push** — sinon ton prochain `git pull` sera bloqué par les modifications locales : ```bash git add -A ios/App && git commit -m "chore(xcode): App Groups sur App, LiveActivity et Watch" && git push ``` ## Étape 4b — Les trois fichiers du widget « Saisie rapide » *(~5 min)* Ils existent dans le repo depuis le 6 août mais n'appartiennent à aucune target : tant qu'ils n'y sont pas, le widget eau/café n'existe pas et l'extension ne compile pas. Dans Xcode, clic droit sur le dossier de la cible → **Add Files to "App"…** → **décoche** *Copy items if needed* → coche la ou les targets du tableau. Pour un fichier déjà présent dans l'arborescence, passe par l'inspecteur de droite (`Cmd+Option+1`) → **Target Membership**. | Fichier | Cocher | |---|---| | `ios/App/App/CoachQuickLog.swift` | **App** *et* **CoachLiveActivity** | | `ios/App/App/CoachQuickSync.swift` | **App** seule | | `ios/App/CoachLiveActivity/CoachQuickWidget.swift` | **CoachLiveActivity** seule | `CoachQuickLog.swift` doit bien être dans **les deux** cibles : l'extension écrit la file d'attente, l'app la vide. Rien d'autre à câbler — `CoachQuickWidget()` est déjà dans le `WidgetBundle` et `CoachQuickSync.flush()` est déjà appelé par `AppDelegate`. Puis commit + push. ## Étape 5 — Complications Watch *(facultative, ~15 min)* **Tu peux sauter cette étape** et y revenir plus tard : tout le reste fonctionne sans. Elle n'ajoute que les complications sur le cadran de la montre. *Réalisée le 2026-08-11 — les écarts constatés sont intégrés ci-dessous.* 1. Menu **File → New → Target…** 2. Onglet **watchOS** → **Widget Extension** → **Next** 3. Product Name : `CoachWatchWidgets`. Les cases proposées ne sont plus celles du guide d'origine : Xcode propose désormais **Include Control** et **Include Configuration App Intent** — **décoche les deux** (nos complications sont des `StaticConfiguration`, sans paramètre à régler). **Embed in Application** : `CoachWatch` → **Finish** 4. Xcode propose d'activer le nouveau schéma → **Activate** 5. **Supprime le fichier template.** Il ne s'appelle pas `CoachWatchWidgets.swift` mais **`CoachWatchWidgetsBundle.swift`**, et il porte un `@main` — or `CoachWatchComplications.swift` en a déjà un, et **deux `@main` dans une target ne compilent pas**. Le dossier étant synchronisé (point 6), un `rm ios/App/CoachWatchWidgets/CoachWatchWidgetsBundle.swift` suffit, Xcode le répercute. Bonne surprise : notre fichier déclare `struct CoachWatchWidgetsBundle`, le nom exact du template — rien à renommer. 6. **Étape sans objet sur Xcode récent.** Les nouvelles targets sont créées en **dossier synchronisé** (`PBXFileSystemSynchronizedRootGroup`) : tout fichier présent dans le dossier de la target en est membre automatiquement, sans être listé dans `project.pbxproj`. `CoachWatchComplications.swift` y était déjà, il est donc déjà inclus — d'où son absence du dialogue *Add Files*, et un `grep CoachWatchComplications project.pbxproj` qui renvoie `0` **sans que ce soit un problème**. Vérifier plutôt : `grep -c PBXFileSystemSynchronizedRootGroup project.pbxproj` ≥ 1. 7. `CoachWidgetSnapshot.swift` vit dans `ios/App/App/`, **hors** du dossier synchronisé : lui doit être coché à la main. `Cmd+Shift+O` → tape son nom → `Cmd+Option+1` → **Target Membership** → coche **CoachWatchWidgets** sans décocher le reste. Sans lui : `cannot find 'CoachWidgetStore' in scope`. 8. Target `CoachWatchWidgets` → **Signing & Capabilities** → **+ Capability** → **App Groups** → coche `group.ch.hypnotruck.coach` 9. ⚠️ **Aligner le numéro de build.** Xcode crée la target en build `1` alors que le projet est en `22` → `The CFBundleVersion of an app extension ('1') must match that of its containing parent app ('22')`. Target → **General** → *Identity* → **Version** `1.2`, **Build** `22`. Contrôle : `grep -n CURRENT_PROJECT_VERSION project.pbxproj | grep -v "= 22;"` ne doit rien renvoyer. Puis commit + push (même raison qu'à l'étape 4). ### Vérifier l'assemblage sans cliquer partout `Cmd+S` d'abord (Xcode n'écrit le projet sur disque qu'à l'enregistrement) : ```bash # à quelle target appartient la phase qui embarque l'extension ? python3 - <<'PY' import re, pathlib s = pathlib.Path('ios/App/App.xcodeproj/project.pbxproj').read_text() emb = dict(re.findall(r'(\w+) /\* (Embed \w[\w ]*) \*/ = \{', s)) for m in re.finditer(r'/\* ([\w.-]+) \*/ = \{\s*isa = PBXNativeTarget;(.*?)\n\t\t\};', s, re.S): for pid, pname in emb.items(): if pid in m.group(2): print(f"{pname:<32} -> target {m.group(1)}") PY ``` `Embed Foundation Extensions -> target CoachWatch` : l'extension part bien sur la montre. Si elle est rattachée à `App`, elle reste sur l'iPhone et aucune complication n'apparaîtra jamais. ⚠️ **La Watch n'a pas besoin d'apparaître comme destination dans Xcode.** La phase *Embed Watch Content* embarque `CoachWatch.app` dans l'app iPhone : builder le schéma **App** sur l'iPhone installe l'app Watch et son extension. Pour voir la montre comme destination il faudrait activer le **Mode développeur** dessus (Réglages → Confidentialité et sécurité), ce qui n'est pas nécessaire ici. ## Étape 6 — Compiler et tester sur l'iPhone 1. Branche l'iPhone, déverrouille-le, avec l'Apple Watch au poignet et appairée 2. En haut de Xcode, sélectionne le schéma **App** et ton iPhone comme device 3. **Product → Clean Build Folder** (`Cmd+Shift+K`) 4. **Run** (`Cmd+R`) ### Si ça ne compile pas | Erreur | Cause | Solution | |---|---|---| | `cannot find 'CoachAuth' in scope` | étape 2 sautée | crée `CoachAuth.swift` | | `no such module 'Capacitor'` | sync manquant | `npx cap sync ios`, Xcode fermé | | erreur de signing | profil pas régénéré | Signing & Capabilities → décoche/recoche *Automatically manage signing* | | `CoachWidgetStore` introuvable | Target Membership | vérifie que `CoachWidgetSnapshot.swift` est coché pour la target qui râle | ### Tests, dans l'ordre (5 min) 1. **L'app se lance et tu es connecté** → le cookie natif fonctionne. 2. **Live workout** (le seul flux déjà validé, à ne pas casser) : démarre une séance sur la Watch → la FC doit apparaître sur `/live` de l'iPhone. ⚠️ *C'est le test le plus important : j'ai touché à `CoachLiveBridge` pour faire passer la routine. Si la FC ne remonte plus, dis-le-moi.* 3. **Routine sur la Watch** : ouvre `/routine` sur l'iPhone (ça pousse les blocs vers la montre) → app Coach sur la Watch → **Routine du jour** → les 6 blocs s'affichent → coche-en un → la page iPhone se met à jour **sans recharger**. 4. **Widgets iPhone** : écran d'accueil → appui long → **+** → cherche *Coach* → ajoute « Séance du jour » et « Score de forme ». 5. **Login natif** : déconnecte-toi, teste *Se connecter avec Apple* et *Google*. Vérification serveur de la routine : ```bash ssh ubuntu@83.228.246.229 'cat /home/ubuntu/coach_sportif/data/routine_log.json' ``` **Si tu t'arrêtes ici, c'est déjà un succès** : ton iPhone a tout le natif. --- ## Étape 7 — Préparer la distribution Deux réglages avant d'archiver. **a. Numéro de build** — target **App** → onglet **General** : `Version` = `1.3`, `Build` = `23` (le repo est à 1.2 / 22). **b. Push notifications en production** — `ios/App/App/App.entitlements` contient `aps-environment = development`. En TestFlight, les notifications push distantes ne marcheront pas avec cette valeur. Le plus simple : laisse Xcode gérer. Avec *Automatically manage signing*, le profil de distribution force `production` à l'archivage. **Vérifie-le après l'étape 8** : Organizer → clic droit sur l'archive → *Show in Finder* → clic droit → *Afficher le contenu* → `Products/Applications/App.app` → clic droit → *Afficher le contenu* → ouvre `embedded.mobileprovision` avec TextEdit → cherche `aps-environment`, il doit valoir `production`. Si ce n'est pas le cas, édite `App.entitlements` en mettant `production`, ré-archive, et **remets `development`** ensuite pour ne pas casser tes builds de dev. ## Étape 8 — Archiver 1. En haut, remplace ton iPhone par **Any iOS Device (arm64)** 2. **Product → Archive** (compte 5-10 min) 3. La fenêtre **Organizer** s'ouvre sur l'archive Si *Archive* est grisé : tu es encore sur un simulateur, repasse en *Any iOS Device*. ## Étape 9 — Envoyer sur TestFlight 1. Dans l'Organizer : **Distribute App** → **App Store Connect** → **Upload** 2. Laisse les options par défaut → **Next** → **Upload** 3. Attends 5-30 min qu'Apple finisse le *processing* (mail de confirmation) 4. [appstoreconnect.apple.com](https://appstoreconnect.apple.com) → **Coach Hypnotruck** → **TestFlight** → le build 23 apparaît 5. ⚠️ **Assigne le build au groupe de testeurs internes** — sans ça, il n'apparaît pas dans TestFlight sur ton iPhone. Ça nous a bloqués en mai : *Internal Testing* → ton groupe → **+** → coche le build 23 6. Sur l'iPhone : app **TestFlight** → **Mettre à jour** ⚠️ Le cache WKWebView est tenace. Si l'app affiche encore l'ancienne interface après mise à jour : **désinstalle l'app** puis réinstalle depuis TestFlight. --- ## En cas de blocage Note l'étape et le message d'erreur exact, et envoie-les-moi. Utile à joindre : ```bash # état du repo cd ~/coach-ios && git log --oneline -3 && git status -sb # les fichiers Swift sont-ils tous câblés ? (doit ne rien afficher sauf # CoachWatchComplications si tu as sauté l'étape 5) # -not -path '*/build/*' est indispensable : DerivedData et les checkouts SPM # contiennent des centaines de .swift qui n'ont rien à faire dans le pbxproj for f in $(find ios -name '*.swift' -not -path '*/build/*' -not -path '*/CapApp-SPM/*'); do grep -q "$(basename $f)" ios/App/App.xcodeproj/project.pbxproj || echo "NON CÂBLÉ: $f" done ``` Pour les erreurs de plugins à l'exécution : Safari → menu **Développement** → ton iPhone → `coach.hypnotruck.ch` → console : ```js Object.keys(Capacitor.Plugins).filter(k => k.startsWith('Coach')) // doit lister : CoachWorkoutKit, CoachHealthRoute, CoachWorkoutObserver, // CoachLiveBridge, CoachAppleAuth, CoachGoogleAuth, // CoachWidgetBridge, CoachRoutineBridge ``` ## Rappels permanents - **Toujours quitter Xcode avant un `git pull`.** - **Toujours commit + push après une modification faite dans Xcode** (capability, signing, target) — sinon le prochain pull est bloqué. - Les changements **web** (templates, CSS, routes) ne demandent **aucun build** : ils arrivent au prochain lancement de l'app.