La première version conditionnait la création de la fenêtre à `window == nil`, par prudence : le manifeste déclarant UISceneStoryboardFile = Main, je supposais qu'UIKit avait déjà monté la fenêtre depuis le storyboard et qu'il ne fallait pas l'écraser. Sur appareil, le résultat est un écran noir permanent, sans erreur, sans log, sans crash. UIKit fournit bien un objet fenêtre, mais ne le rend pas visible à notre place : la garde sautait donc makeKeyAndVisible() et rien ne s'affichait jamais. Le fond noir de capacitor.config.ts rendait le symptôme indiscernable d'un écran d'attente. Le template de Capacitor crée la fenêtre sans condition. C'est le chemin éprouvé, et s'en écarter « par sécurité » aura coûté un aller-retour de build. Le seul écart qui reste nécessaire est MainViewController au lieu d'un CAPBridgeViewController nu : notre sous-classe enregistre les neuf plugins in-app dans capacitorDidLoad(), et ne dépend de rien du storyboard. ⚠️ À retenir pour la suite : sur ce chemin, une garde défensive ne protège de rien et masque le défaut qu'elle prétend éviter. La condition de succès était observable (une fenêtre visible), pas déductible. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
85 lines
4.4 KiB
Swift
85 lines
4.4 KiB
Swift
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 }
|
||
|
||
// ⚠️ Ne PAS conditionner cette création à `window == nil`.
|
||
//
|
||
// C'était ma première version, par prudence : le manifeste déclarant
|
||
// `UISceneStoryboardFile = Main`, je supposais qu'UIKit avait déjà
|
||
// monté la fenêtre et qu'il ne fallait pas l'écraser. Résultat sur
|
||
// appareil : **écran noir permanent, sans la moindre erreur**. UIKit
|
||
// fournit bien une fenêtre, mais ne la rend pas visible à notre place ;
|
||
// la garde sautait donc `makeKeyAndVisible()` et rien ne s'affichait.
|
||
//
|
||
// Le template de Capacitor crée la fenêtre sans condition, et c'est le
|
||
// chemin éprouvé. Le seul écart nécessaire est `MainViewController` au
|
||
// lieu d'un `CAPBridgeViewController` nu : notre sous-classe enregistre
|
||
// les neuf plugins in-app dans `capacitorDidLoad()`. Elle ne dépend de
|
||
// rien du storyboard, donc l'instancier ici est sans perte.
|
||
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() }
|
||
}
|
||
}
|