Merge branch 'feat/watch-outdoor' of https://git.nexus.hypnotruck.ch/sylvain/coach-ios into feat/watch-outdoor
This commit is contained in:
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
81
ios/App/App/SceneDelegate.swift
Normal file
81
ios/App/App/SceneDelegate.swift
Normal 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() }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user