From 3755cafa1fec7e99133b39cb5ef0b64a43e0d552 Mon Sep 17 00:00:00 2001 From: Sylvain Bettinelli Date: Tue, 22 Sep 2026 08:04:14 +0000 Subject: [PATCH 1/3] =?UTF-8?q?Adoption=20du=20cycle=20de=20vie=20UIScene?= =?UTF-8?q?=20:=20sans=20lui,=20l'app=20ne=20d=C3=A9marre=20plus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le build échouait au lancement sur _UIApplicationEvaluateRuntimeIssueForNo SceneLifecycleAdoption. Apple est explicite : « Beginning in iOS 27 […] apps built with the latest SDK must adopt the scene-based life cycle or they fail to launch » (Transitioning to the UIKit scene-based life cycle). Ce n'est plus un avertissement comme en iOS 26, c'est un assert. Aucune dérogation n'est documentée : ni clé Info.plist, ni entitlement, ni date de tolérance — le seul report possible serait de ne pas construire avec le dernier SDK, ce qui ne fait que déplacer l'échéance sous contrainte de release. Capacitor a adopté UIScene en 8.5.0 (PR #8536, « required in iOS 27 »), et 8.5.2 corrige une régression qui faisait échouer chaque démarrage à froid sur une JS Eval error (#8595). On passe donc de 8.3.1 à 8.5.2, pas à 8.5.0. ⚠️ npx cap migrate N'A PAS été utilisé, et c'est volontaire : le guide officiel exclut les projets à gestion d'URL personnalisée dans l'AppDelegate — « Projects with hand-rolled scene delegates or custom AppDelegate URL handling should follow the manual steps instead ». Notre branche GoogleSignIn en est la définition. Le SceneDelegate suit le template livré dans le paquet 8.5.2, avec deux écarts nécessaires décrits ci-dessous. ⚠️ CE QUI CASSE EN SILENCE, et qui est repris dans SceneDelegate.swift. Une fois le manifeste posé, UIKit cesse d'appeler trois méthodes de l'AppDelegate « even if you keep them implemented » (Apple) : - application(_:open:options:) -> le retour OAuth Google partirait vers Safari sans jamais revenir. Déprécié iOS 26, remplacé par scene(_:openURLContexts:). La signature change : une URL devient un Set, donc on filtre ce que Google consomme avant de passer le reste à Capacitor. - applicationDidBecomeActive -> les saisies du widget (eau, café) resteraient bloquées dans l'App Group. Reprise dans sceneDidBecomeActive. - application(_:continue:restorationHandler:) -> les Universal Links des widgets et complications ouvriraient Safari, où le cookie d'auth n'existe pas. Aucune des trois ne produit d'erreur de compilation. Elles ne sont pas laissées en doublon dans l'AppDelegate : une méthode morte qui a l'air vivante fait chercher la panne au mauvais endroit. ✅ CE QUI NE BOUGE PAS, vérifié dans la doc avant d'écrire une ligne : didFinishLaunchingWithOptions reste appelé, et Apple documente la séquence (launch -> création des scènes -> scene(_:willConnectTo:)). L'injection du cookie garde donc son ordre : elle s'exécute avant que la WKWebView charge. La règle non négociable n°2 du CLAUDE.md est tenue. Les callbacks de notification distante restent aussi sur l'AppDelegate — UISceneDelegate n'a aucun équivalent et ils ne sont pas dépréciés. Écart au template Capacitor : il pose un CAPBridgeViewController() nu comme rootViewController. L'appliquer tel quel remplacerait MainViewController, qui porte l'enregistrement des 9 plugins natifs et la config WKWebView — sans erreur visible. Le manifeste gardant UISceneStoryboardFile = Main, UIKit charge déjà le storyboard : on ne crée la fenêtre que s'il ne l'a pas fait, et avec notre sous-classe. npm audit : 0 vulnérabilité en production. 13 dans l'outillage de build, qui ne part pas dans l'app. ⛔ Ne PAS lancer npm audit fix --force : il propose de redescendre @capacitor/cli en 7.6.9, ce qui supprimerait le support UIScene. ⚠️ RESTE À FAIRE SUR LE MAC, et rien ne compilera sans : ajouter SceneDelegate.swift au target App dans Xcode. Le dossier App/ utilise des références explicites dans le pbxproj (les groupes synchronisés ne couvrent que les targets Watch/Widgets), et je n'édite pas un pbxproj à l'aveugle. Vérifié ici : plist valide et relu par plistlib, syntaxe des deux fichiers Swift validée par swiftc -parse. Aucune compilation possible sur Linux. Co-Authored-By: Claude Opus 5 (1M context) --- ios/App/App/AppDelegate.swift | 51 +++++++++++---------- ios/App/App/Info.plist | 19 ++++++++ ios/App/App/SceneDelegate.swift | 81 +++++++++++++++++++++++++++++++++ package-lock.json | 40 ++++++++-------- package.json | 8 ++-- 5 files changed, 152 insertions(+), 47 deletions(-) create mode 100644 ios/App/App/SceneDelegate.swift diff --git a/ios/App/App/AppDelegate.swift b/ios/App/App/AppDelegate.swift index 6575b45..54b8077 100644 --- a/ios/App/App/AppDelegate.swift +++ b/ios/App/App/AppDelegate.swift @@ -1,12 +1,11 @@ import UIKit import Capacitor -#if canImport(GoogleSignIn) -import GoogleSignIn -#endif @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { + // Conservée comme dans le template Capacitor 8.5. En cycle par scènes, + // c'est le SceneDelegate qui possède la fenêtre affichée. var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { @@ -47,28 +46,30 @@ class AppDelegate: UIResponder, UIApplicationDelegate { HTTPCookieStorage.shared.setCookie(cookie) } - func applicationWillResignActive(_ application: UIApplication) {} - func applicationDidEnterBackground(_ application: UIApplication) {} - func applicationWillEnterForeground(_ application: UIApplication) {} - func applicationDidBecomeActive(_ application: UIApplication) { - // Les saisies faites depuis le widget (eau, café) attendent dans l'App - // Group : une extension n'a ni session ni droit de tenir une requête. - // On les transmet ici, à chaque retour au premier plan. - Task { await CoachQuickSync.flush() } + // ─── Cycle de vie par scènes (obligatoire iOS 27) ─────────────────── + // + // ⚠️ Depuis l'adoption du `UIApplicationSceneManifest`, UIKit n'appelle + // PLUS `applicationDidBecomeActive`, `application(_:open:options:)` ni + // `application(_:continue:restorationHandler:)` — « even if you keep them + // implemented » (Apple, *Transitioning to the UIKit scene-based life + // cycle*). Les trois ont déménagé dans `SceneDelegate.swift` : + // + // applicationDidBecomeActive -> sceneDidBecomeActive (CoachQuickSync) + // application(_:open:options:) -> scene(_:openURLContexts:) (OAuth Google) + // application(_:continue:…) -> scene(_:continue:) (Universal Links) + // + // Elles ne sont pas conservées ici en doublon : une méthode morte qui a + // l'air vivante est exactement ce qui fait chercher la panne au mauvais + // endroit. Les callbacks de notification distante, eux, restent sur + // l'AppDelegate — `UISceneDelegate` n'en a aucun équivalent. + func application(_ application: UIApplication, + configurationForConnecting connectingSceneSession: UISceneSession, + options: UIScene.ConnectionOptions) -> UISceneConfiguration { + let config = UISceneConfiguration(name: "Default Configuration", + sessionRole: connectingSceneSession.role) + config.delegateClass = SceneDelegate.self + return config } + func applicationWillTerminate(_ application: UIApplication) {} - - func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { - #if canImport(GoogleSignIn) - // Retour du flux OAuth Google (reversed client id URL scheme). - if GIDSignIn.sharedInstance.handle(url) { - return true - } - #endif - return ApplicationDelegateProxy.shared.application(app, open: url, options: options) - } - - func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { - return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler) - } } diff --git a/ios/App/App/Info.plist b/ios/App/App/Info.plist index ab76977..452998b 100644 --- a/ios/App/App/Info.plist +++ b/ios/App/App/Info.plist @@ -50,6 +50,25 @@ location fetch + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile diff --git a/ios/App/App/SceneDelegate.swift b/ios/App/App/SceneDelegate.swift new file mode 100644 index 0000000..367209d --- /dev/null +++ b/ios/App/App/SceneDelegate.swift @@ -0,0 +1,81 @@ +import UIKit +import Capacitor +#if canImport(GoogleSignIn) +import GoogleSignIn +#endif + +/// Cycle de vie par scènes, **obligatoire** depuis iOS 27 : « apps built with +/// the latest SDK must adopt the scene-based life cycle or they fail to launch » +/// (Apple, *Transitioning to the UIKit scene-based life cycle*). Sans ce +/// fichier et le `UIApplicationSceneManifest` de l'Info.plist, l'app ne démarre +/// pas du tout — c'est un assert au lancement, pas un avertissement. +/// +/// ⚠️ **Ce qui arrive ici arrive en SILENCE.** Une fois le manifeste en place, +/// UIKit cesse d'appeler `application(_:open:options:)`, +/// `application(_:continue:restorationHandler:)` et `applicationDidBecomeActive` +/// sur l'AppDelegate — « even if you keep them implemented » (Apple). Rien ne +/// casse à la compilation : le retour OAuth Google part vers Safari et ne +/// revient jamais, et les saisies du widget restent bloquées dans l'App Group. +/// Les trois sont donc repris ci-dessous. +/// +/// Ce que la migration NE change PAS, et c'est ce qui la rendait acceptable : +/// `application(_:didFinishLaunchingWithOptions:)` reste appelé, et toujours +/// **avant** que la scène ne se connecte — donc avant que la WKWebView charge. +/// L'injection du cookie d'auth garde son ordre (Apple documente la séquence : +/// launch → création des scènes → `scene(_:willConnectTo:)`). Les callbacks de +/// notification distante ne bougent pas non plus : `UISceneDelegate` n'a aucun +/// équivalent, et ils ne sont pas dépréciés. +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + var window: UIWindow? + + func scene(_ scene: UIScene, + willConnectTo session: UISceneSession, + options connectionOptions: UIScene.ConnectionOptions) { + guard let windowScene = scene as? UIWindowScene else { return } + + // Le manifeste déclare `UISceneStoryboardFile = Main` : UIKit a donc + // déjà chargé Main.storyboard et instancié `MainViewController`, qui + // porte l'enregistrement des plugins natifs et la config WKWebView. + // + // Le template de Capacitor pose ici un `CAPBridgeViewController()` nu. + // L'appliquer tel quel remplacerait notre sous-classe et ferait perdre + // cette configuration **sans erreur visible**. On ne crée donc la + // fenêtre que si le storyboard ne l'a pas fait, et avec notre + // contrôleur. + if window == nil { + let fenetre = UIWindow(windowScene: windowScene) + fenetre.rootViewController = MainViewController() + window = fenetre + fenetre.makeKeyAndVisible() + } + + SceneDelegateProxy.shared.scene(scene, willConnectTo: session, options: connectionOptions) + } + + func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { + // Retour du flux OAuth Google. `handle(url)` prend UNE url alors que la + // scène en livre un ensemble : on laisse à Capacitor ce que Google n'a + // pas consommé, plutôt que de tout lui passer ou de tout lui cacher. + var restants = URLContexts + #if canImport(GoogleSignIn) + restants = URLContexts.filter { !GIDSignIn.sharedInstance.handle($0.url) } + #endif + guard !restants.isEmpty else { return } + SceneDelegateProxy.shared.scene(scene, openURLContexts: restants) + } + + func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { + // Universal Links : les .widgetURL des widgets, de la Live Activity et + // des complications Watch passent par là. Sans ce relais, ils + // ouvriraient Safari, où le cookie injecté dans la WKWebView n'existe + // pas. + SceneDelegateProxy.shared.scene(scene, continue: userActivity) + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Reprend `applicationDidBecomeActive`, qu'UIKit n'appelle plus. Les + // saisies faites depuis le widget (eau, café) attendent dans l'App + // Group : une extension n'a ni session ni droit de tenir une requête. + Task { await CoachQuickSync.flush() } + } +} diff --git a/package-lock.json b/package-lock.json index 752f665..5e06f5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,12 +9,12 @@ "version": "0.1.0", "license": "UNLICENSED", "dependencies": { - "@capacitor/android": "^8.3.1", + "@capacitor/android": "^8.5.2", "@capacitor/app": "^8.1.0", - "@capacitor/core": "^8.3.1", + "@capacitor/core": "^8.5.2", "@capacitor/geolocation": "^8.2.0", "@capacitor/haptics": "^8.0.2", - "@capacitor/ios": "^8.3.1", + "@capacitor/ios": "^8.5.2", "@capacitor/local-notifications": "^8.0.2", "@capacitor/push-notifications": "^8.0.3", "@capacitor/splash-screen": "^8.0.1", @@ -24,7 +24,7 @@ }, "devDependencies": { "@capacitor/assets": "^3.0.5", - "@capacitor/cli": "^8.3.1" + "@capacitor/cli": "^8.5.2" } }, "node_modules/@babel/code-frame": { @@ -51,12 +51,12 @@ } }, "node_modules/@capacitor/android": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/@capacitor/android/-/android-8.3.1.tgz", - "integrity": "sha512-hjskIG8YcBEh3X4yaTXvE9gcqpdcxunTgFruSKnuPxtMxAUzEK4Oq25x0Z1g3cz+MQPc+lRG09R7Ovc+ydKsNw==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@capacitor/android/-/android-8.5.2.tgz", + "integrity": "sha512-rtiaz78egCcS+vs29wkWCL3C+RhazKMSygXcjutwWP8NmO6r5UBxOvGf3Wjz5+ahobyQ723tn9gCePwRwcNDZQ==", "license": "MIT", "peerDependencies": { - "@capacitor/core": "^8.3.0" + "@capacitor/core": "^8.5.0" } }, "node_modules/@capacitor/app": { @@ -452,10 +452,11 @@ "dev": true }, "node_modules/@capacitor/cli": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-8.3.1.tgz", - "integrity": "sha512-1sPGW4THTDfR6YjXwZ0jM7oAfAtciPOHN00qs/3sNAQx1kKrrEYSfDPwCm1/xlAgi0OeL69SiRfw314Ans+1sw==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-8.5.2.tgz", + "integrity": "sha512-O2kpA5ZPJUAmliDCcmpMf8xnJwMezxlFPUrlyOAjbL3U9MFNY3l4ZpPI/UXwvyPmR8ETcVF9pFr+VdWq61rqTQ==", "dev": true, + "license": "MIT", "dependencies": { "@ionic/cli-framework-output": "^2.2.8", "@ionic/utils-subprocess": "^3.0.1", @@ -473,6 +474,7 @@ "semver": "^7.6.3", "tar": "^7.5.3", "tslib": "^2.8.1", + "xcode": "^3.0.1", "xml2js": "^0.6.2" }, "bin": { @@ -484,9 +486,10 @@ } }, "node_modules/@capacitor/core": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-8.3.1.tgz", - "integrity": "sha512-UF8ItlHguU1Z6GXfPTeT2gakf+ctNI8pAS1kwSBQlsJMlfD4OPoto/SmKnOxKCQvnF4WRcdWeg6C0zREUNaAQg==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-8.5.2.tgz", + "integrity": "sha512-agnQbGdhGw/KZFuLf7H9SY2o+oE7upkr/WPQsYaokXP6f9chaE8iVmbKkfN6SVl4w3pmz7oZ3Ur1hbkmy1hx+w==", + "license": "MIT", "dependencies": { "tslib": "^2.1.0" } @@ -511,11 +514,12 @@ } }, "node_modules/@capacitor/ios": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-8.3.1.tgz", - "integrity": "sha512-BEhLyYYHWJLib4mpaPMaaylbC8meqgxbNYwQJH2svsSLW7yo/hFie+Zoo66a44XnqcMd2tvmAuzimWunXZi/xA==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-8.5.2.tgz", + "integrity": "sha512-eOT2S67AfTFG7KN0T3oxwAajrJz4rSye+ODfvChiwrk0F/+kWufFM1qU6cDjGJUOfwdAoeuEpZyWHSwZlu4GFg==", + "license": "MIT", "peerDependencies": { - "@capacitor/core": "^8.3.0" + "@capacitor/core": "^8.5.0" } }, "node_modules/@capacitor/local-notifications": { diff --git a/package.json b/package.json index 7a5f6a8..13c968f 100644 --- a/package.json +++ b/package.json @@ -15,12 +15,12 @@ "author": "Sylvain Bettinelli", "license": "UNLICENSED", "dependencies": { - "@capacitor/android": "^8.3.1", + "@capacitor/android": "^8.5.2", "@capacitor/app": "^8.1.0", - "@capacitor/core": "^8.3.1", + "@capacitor/core": "^8.5.2", "@capacitor/geolocation": "^8.2.0", "@capacitor/haptics": "^8.0.2", - "@capacitor/ios": "^8.3.1", + "@capacitor/ios": "^8.5.2", "@capacitor/local-notifications": "^8.0.2", "@capacitor/push-notifications": "^8.0.3", "@capacitor/splash-screen": "^8.0.1", @@ -30,6 +30,6 @@ }, "devDependencies": { "@capacitor/assets": "^3.0.5", - "@capacitor/cli": "^8.3.1" + "@capacitor/cli": "^8.5.2" } } From 3bd90c8bb794a5e35d648d2ab8f9712d65eaa153 Mon Sep 17 00:00:00 2001 From: Sylvain Bettinelli Date: Tue, 22 Sep 2026 08:05:25 +0000 Subject: [PATCH 2/3] =?UTF-8?q?CLAUDE.md=20:=20la=20r=C3=A8gle=20du=20cook?= =?UTF-8?q?ie=20ne=20suffit=20plus,=20le=20SceneDelegate=20en=20porte=20la?= =?UTF-8?q?=20moiti=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La règle n°2 protégeait AppDelegate.swift. Depuis l'adoption d'UIScene, trois callbacks ont déménagé et UIKit ne les appelle plus sur l'AppDelegate même si on les y remet — un futur correctif posé au mauvais endroit compilerait sans broncher et casserait l'OAuth ou le flush des saisies widget en silence. Précisé au passage que didFinishLaunchingWithOptions reste appelé avant la connexion de la scène : c'est ce qui garantit l'ordre cookie -> WebView, et c'est documenté par Apple, pas supposé. Deux commandes ajoutées à la liste de ce qu'on ne lance pas sur ce projet : npx cap migrate (le guide officiel exclut les AppDelegate à gestion d'URL personnalisée) et npm audit fix --force (il redescend le CLI en v7, ce qui supprimerait le support UIScene qu'on vient d'ajouter). Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index c7c6676..2f2be7e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,8 @@ Instructions pour Claude Code sur ce projet. Lis ce fichier en début de session ## 🔒 Règles non négociables 1. **Le code web n'est PAS ici** — pour modifier l'UI/UX, c'est `coach_sportif/web/`. Ce repo ne touche QUE le natif (Swift, Kotlin, plugins Capacitor, config). -2. **Cookie inject AppDelegate** (iOS) : mécanisme critique pour l'auth WKWebView. Toute modif `AppDelegate.swift` doit préserver ce flux. +2. **Cookie inject AppDelegate** (iOS) : mécanisme critique pour l'auth WKWebView. Toute modif `AppDelegate.swift` doit préserver ce flux. Il vit dans `didFinishLaunchingWithOptions`, qui **reste appelé** en cycle de vie par scènes et **avant** la connexion de la scène — Apple documente la séquence, l'ordre cookie → WebView est donc garanti. +2bis. **⚠️ Cycle de vie UIScene depuis le 22/09/2026** (obligatoire iOS 27, Capacitor ≥ 8.5.2). Trois callbacks ont quitté `AppDelegate.swift` pour `SceneDelegate.swift`, et UIKit **ne les appelle plus sur l'AppDelegate même si on les y remet** : retour OAuth Google (`scene(_:openURLContexts:)`), Universal Links (`scene(_:continue:)`), flush des saisies widget (`sceneDidBecomeActive`). Aucun ne produit d'erreur de compilation s'il est mal placé — la panne est silencieuse. Le `SceneDelegate` instancie **`MainViewController`**, jamais un `CAPBridgeViewController` nu comme le template Capacitor : la sous-classe porte l'enregistrement des plugins et la config WKWebView. ⛔ Ne pas lancer `npx cap migrate` sur ce projet (le guide officiel exclut les AppDelegate à gestion d'URL personnalisée), ni `npm audit fix --force` (il redescend le CLI en v7 et supprime le support UIScene). 3. **Health Connect Android** : nécessite la privacy policy URL dans le manifest (validation Play Console). 4. **Pas d'extrapolation** : root cause vérifiable avant tout fix natif. iOS+Android = doubles surfaces de bug. 5. **Default yes** : décider et exécuter (sauf actions destructives). From e5c8967ea913b1eba2db330f22173ec5a9ad53a9 Mon Sep 17 00:00:00 2001 From: Sylvain Bettinelli Date: Tue, 22 Sep 2026 08:15:41 +0000 Subject: [PATCH 3/3] =?UTF-8?q?SceneDelegate.swift=20enregistr=C3=A9=20dan?= =?UTF-8?q?s=20le=20target=20App?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sans cette entrée, Xcode ne compile pas le fichier et SceneDelegate.self reste introuvable : l'app ne démarrerait pas davantage qu'avant la migration. Fait avec la bibliothèque `xcode` que Capacitor utilise lui-même pour cette opération (addSwiftFileToAppTarget), pas à la main. Essai à blanc d'abord pour confirmer la cible : getFirstTarget() rend bien « App » et non l'un des trois autres targets (CoachWatch, CoachWatchWidgetsExtension, CoachLiveActivity). Deux retouches après coup : - la bibliothèque écrivait `explicitFileType = undefined` sur la référence. explicitFileType prime sur la détection automatique, et cette valeur aurait pu empêcher Xcode de reconnaître le fichier comme du Swift. Normalisé au format exact d'AppDelegate.swift, ligne pour ligne ; - elle a reformaté le groupe synchronisé CoachWatchWidgets de sa forme compacte vers le format multi-lignes. Vérifié champ par champ : même isa, même UUID d'exception, mêmes explicitFileTypes et explicitFolders vides, même path. Reformatage pur, aucune perte. Intégrité vérifiée en comparant les deux fichiers parsés, objet par objet : hors nos deux ajouts, aucune autre différence. Le groupe App passe de 28 à 29 enfants et la phase Sources de 20 à 21 fichiers, avec zéro retrait de part et d'autre. Le projet se reparse et les quatre targets sont intacts. Co-Authored-By: Claude Opus 5 (1M context) --- ios/App/App.xcodeproj/project.pbxproj | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index 462be68..336b67c 100644 --- a/ios/App/App.xcodeproj/project.pbxproj +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -68,6 +68,7 @@ E547F675B2F61C3475D2A787 /* LiveActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B93DA6B121E94B11DF938C90 /* LiveActivityManager.swift */; }; F1ABCDEF0123456789AB0001 /* CoachHealthRoute.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1ABCDEF0123456789AB0002 /* CoachHealthRoute.swift */; }; F1ABCDEF0123456789AB0003 /* CoachSleep.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1ABCDEF0123456789AB0004 /* CoachSleep.swift */; }; + 60BB59C4096F4C8EB22DE183 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B82F240520FD4C6CB4A80522 /* SceneDelegate.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -190,6 +191,7 @@ F1ABCDEF0123456789AB0004 /* CoachSleep.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoachSleep.swift; sourceTree = ""; }; F6BEAEF6F6274D61CAF22636 /* CoachWatchApp.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CoachWatchApp.swift; sourceTree = ""; }; FBFCA969A1B083CD3178D9B2 /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + B82F240520FD4C6CB4A80522 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ @@ -203,7 +205,18 @@ /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ - 880A76D4302B601000F582D5 /* CoachWatchWidgets */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (880A76E2302B601100F582D5 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = CoachWatchWidgets; sourceTree = ""; }; + 880A76D4302B601000F582D5 /* CoachWatchWidgets */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 880A76E2302B601100F582D5 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, + ); + explicitFileTypes = { + }; + explicitFolders = ( + ); + path = CoachWatchWidgets; + sourceTree = ""; + }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ @@ -322,6 +335,7 @@ 6030BD533EBC2D1D26DB9479 /* LiveStore.swift */, 340B99D5EA4DB985D7DFDA10 /* CoachLiveView.swift */, B93DA6B121E94B11DF938C90 /* LiveActivityManager.swift */, + B82F240520FD4C6CB4A80522 /* SceneDelegate.swift */, ); path = App; sourceTree = ""; @@ -577,6 +591,7 @@ 548F8BFA06417B35474BD69E /* CoachLiveView.swift in Sources */, D1384CFA4960060CDBF05FC3 /* CoachLiveActivityAttributes.swift in Sources */, E547F675B2F61C3475D2A787 /* LiveActivityManager.swift in Sources */, + 60BB59C4096F4C8EB22DE183 /* SceneDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; };