L'etape 5 est faite. Cinq ecarts entre le guide et Xcode reel, tous corriges dans le doc pour que la prochaine session ne les repaye pas : Le template ne s'appelle plus CoachWatchWidgets.swift mais CoachWatchWidgetsBundle.swift. Comme il porte un @main et que CoachWatchComplications.swift en a deja un, l'oublier fait echouer la compilation sur deux @main dans la meme target. Les nouvelles targets sont creees en dossier synchronise : tout fichier du dossier en est membre automatiquement, sans etre liste dans project.pbxproj. CoachWatchComplications.swift etait donc deja inclus - d'ou son absence du dialogue Add Files, qui ressemble a une panne et n'en est pas une. L'etape 6 du guide est desormais sans objet, et un grep qui renvoie 0 sur ce fichier n'est plus un signal d'alarme. Xcode cree la target en build 1 quand le projet est en 22, ce qui bloque a l'installation. Les cases du template ont change (Include Control et Include Configuration App Intent au lieu de Include Live Activity) : les deux se decochent, nos complications sont des StaticConfiguration. Et la Watch n'a pas besoin d'etre visible comme destination dans Xcode : la phase Embed Watch Content embarque l'app Watch dans l'app iPhone, builder le schema App suffit. Chercher la montre dans la liste des devices est une impasse qui coute un mode developpeur et un redemarrage pour rien. Ajout d'un script de verification de l'assemblage : il dit a quelle target appartient chaque phase Embed. Embed Foundation Extensions doit pointer sur CoachWatch - rattachee a App, l'extension resterait sur l'iPhone et aucune complication n'apparaitrait. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
350 lines
15 KiB
Markdown
350 lines
15 KiB
Markdown
# 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 <TOKEN> par la valeur affichée :
|
|
cat > ios/App/App/CoachAuth.swift <<'EOF'
|
|
import Foundation
|
|
enum CoachAuth { static let kCoachWebToken: String = "<TOKEN>" }
|
|
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.
|