Merge branch 'feat/watch-outdoor' of https://git.nexus.hypnotruck.ch/sylvain/coach-ios into feat/watch-outdoor

This commit is contained in:
Sylvain
2026-09-22 10:33:27 +02:00
7 changed files with 170 additions and 49 deletions

View File

@@ -71,6 +71,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 */
@@ -195,6 +196,7 @@
F1ABCDEF0123456789AB0004 /* CoachSleep.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoachSleep.swift; sourceTree = "<group>"; };
F6BEAEF6F6274D61CAF22636 /* CoachWatchApp.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CoachWatchApp.swift; sourceTree = "<group>"; };
FBFCA969A1B083CD3178D9B2 /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
B82F240520FD4C6CB4A80522 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
@@ -208,7 +210,18 @@
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
880A76D4302B601000F582D5 /* CoachWatchWidgets */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (880A76E2302B601100F582D5 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = CoachWatchWidgets; sourceTree = "<group>"; };
880A76D4302B601000F582D5 /* CoachWatchWidgets */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
880A76E2302B601100F582D5 /* PBXFileSystemSynchronizedBuildFileExceptionSet */,
);
explicitFileTypes = {
};
explicitFolders = (
);
path = CoachWatchWidgets;
sourceTree = "<group>";
};
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
@@ -330,6 +343,7 @@
6030BD533EBC2D1D26DB9479 /* LiveStore.swift */,
340B99D5EA4DB985D7DFDA10 /* CoachLiveView.swift */,
B93DA6B121E94B11DF938C90 /* LiveActivityManager.swift */,
B82F240520FD4C6CB4A80522 /* SceneDelegate.swift */,
);
path = App;
sourceTree = "<group>";
@@ -589,6 +603,7 @@
548F8BFA06417B35474BD69E /* CoachLiveView.swift in Sources */,
D1384CFA4960060CDBF05FC3 /* CoachLiveActivityAttributes.swift in Sources */,
E547F675B2F61C3475D2A787 /* LiveActivityManager.swift in Sources */,
60BB59C4096F4C8EB22DE183 /* SceneDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View File

@@ -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)
}
}

View File

@@ -50,6 +50,25 @@
<string>location</string>
<string>fetch</string>
</array>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneConfigurationName</key>
<string>Default Configuration</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>

View File

@@ -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<UIOpenURLContext>) {
// 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() }
}
}