From 8780486eaeac8ce4d0a0537188c18c3bdedc22ea Mon Sep 17 00:00:00 2001 From: Julius Joska Date: Fri, 23 Oct 2026 22:28:10 +0200 Subject: [PATCH] =?UTF-8?q?fix(ios):=20adopt=20the=20UIScene=20life=20cycl?= =?UTF-8?q?e=20=E2=80=94=201.2.3=20(27)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Build 26 was the first one made with the iOS 27 SDK, and UIKit stops apps without the scene life cycle at launch on iOS 27 (TN3187). The scene delegate only shows the FlutterEngine that AppDelegate starts with the process, so a VoIP push in the background still finds the CallKit plugin and a running Dart isolate when no scene is connected. --- CHANGELOG.md | 6 +++ app/ios/Runner.xcodeproj/project.pbxproj | 4 ++ app/ios/Runner/AppDelegate.swift | 47 ++++++++++++------------ app/ios/Runner/Info.plist | 21 ++++++++++- app/ios/Runner/SceneDelegate.swift | 32 ++++++++++++++++ app/pubspec.yaml | 2 +- 6 files changed, 85 insertions(+), 27 deletions(-) create mode 100644 app/ios/Runner/SceneDelegate.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index e159604..ce2ecbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ verzování dle [ajtak standardu](#verzování). ## [1.2.3] – 2026-10-23 +### Opraveno (iOS, build 27) +- **Na iOS 27 appka hned po spuštění spadla.** Build 26 vznikl novým + Xcode (SDK iOS 27) a ten u appek bez scénového životního cyklu (UIScene) + start zastaví. Appka teď scény používá; Flutter běží od startu procesu, + takže příchozí hovor na pozadí dál zazvoní i bez otevřeného okna. + ### Bezpečnost - **24 slov jen po čerstvém ověření.** Zálohovací fráze se z odemčené appky ukáže až po zadání PINu (nebo otisku/obličeje, je-li zapnutý). Totéž platí diff --git a/app/ios/Runner.xcodeproj/project.pbxproj b/app/ios/Runner.xcodeproj/project.pbxproj index 465bcf0..1e793cb 100644 --- a/app/ios/Runner.xcodeproj/project.pbxproj +++ b/app/ios/Runner.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ AA00000000000000000000A2 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = AA00000000000000000000A1 /* PrivacyInfo.xcprivacy */; }; AB10000000000000000000B1 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = AB10000000000000000000C0 /* InfoPlist.strings */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + AA00000000000000000000B2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00000000000000000000B1 /* SceneDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; @@ -49,6 +50,7 @@ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + AA00000000000000000000B1 /* SceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; @@ -126,6 +128,7 @@ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + AA00000000000000000000B1 /* SceneDelegate.swift */, 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, ); path = Runner; @@ -285,6 +288,7 @@ buildActionMask = 2147483647; files = ( 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + AA00000000000000000000B2 /* SceneDelegate.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/app/ios/Runner/AppDelegate.swift b/app/ios/Runner/AppDelegate.swift index 241178a..f9669a5 100644 --- a/app/ios/Runner/AppDelegate.swift +++ b/app/ios/Runner/AppDelegate.swift @@ -7,6 +7,12 @@ import flutter_callkit_incoming @main @objc class AppDelegate: FlutterAppDelegate, PKPushRegistryDelegate, CallkitIncomingAppDelegate { + // Engine startuje s procesem, ne se scénou. VoIP push umí appku spustit na + // pozadí, aniž by iOS připojil scénu (a tím vytvořil FlutterViewController). + // CallKit plugin musí být zaregistrovaný dřív, než push dorazí, a Dart musí + // běžet, aby vyzvedl nabídku hovoru z relaye. SceneDelegate tenhle engine + // jen zobrazí. + let flutterEngine = FlutterEngine(name: "sepot") // PushKit registry musí žít po celou dobu běhu appky, jinak VoIP pushe nechodí. private var voipRegistry: PKPushRegistry? private var apnsChannel: FlutterMethodChannel? @@ -17,31 +23,24 @@ import flutter_callkit_incoming _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { - // Bez force-castu: kdyby root nebyl FlutterViewController (jiný storyboard, - // budoucí scény), appka by spadla hned při startu. Registrar dá messenger - // enginu i tak. - let messenger = - (window?.rootViewController as? FlutterViewController)?.binaryMessenger - ?? registrar(forPlugin: "SepotApns")?.messenger() - if let messenger { - let channel = FlutterMethodChannel( - name: "cz.sepot.sepot/apns", binaryMessenger: messenger) - channel.setMethodCallHandler { [weak self] call, result in - switch call.method { - case "register": - // Nativní APNs registrace (bez Firebase) — token přijde v callbacku - application.registerForRemoteNotifications() - result(nil) - case "getToken": - result(self?.pendingToken) - default: - result(FlutterMethodNotImplemented) - } - } - apnsChannel = channel - } + flutterEngine.run() + GeneratedPluginRegistrant.register(with: flutterEngine) - GeneratedPluginRegistrant.register(with: self) + let channel = FlutterMethodChannel( + name: "cz.sepot.sepot/apns", binaryMessenger: flutterEngine.binaryMessenger) + channel.setMethodCallHandler { [weak self] call, result in + switch call.method { + case "register": + // Nativní APNs registrace (bez Firebase) — token přijde v callbacku + UIApplication.shared.registerForRemoteNotifications() + result(nil) + case "getToken": + result(self?.pendingToken) + default: + result(FlutterMethodNotImplemented) + } + } + apnsChannel = channel // Hovory: VoIP push z relaye → CallKit. let registry = PKPushRegistry(queue: DispatchQueue.main) diff --git a/app/ios/Runner/Info.plist b/app/ios/Runner/Info.plist index f69cdad..78f5aea 100644 --- a/app/ios/Runner/Info.plist +++ b/app/ios/Runner/Info.plist @@ -48,8 +48,25 @@ UILaunchStoryboardName LaunchScreen - UIMainStoryboardFile - Main + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + flutter + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + + + + UISupportedInterfaceOrientations UIInterfaceOrientationPortrait diff --git a/app/ios/Runner/SceneDelegate.swift b/app/ios/Runner/SceneDelegate.swift new file mode 100644 index 0000000..7c7a180 --- /dev/null +++ b/app/ios/Runner/SceneDelegate.swift @@ -0,0 +1,32 @@ +import Flutter +import UIKit + +// SDK iOS 27 bez scénového životního cyklu appku nespustí (TN3187). Scéna jen +// ukáže engine, který AppDelegate spustil při startu procesu — viz komentář +// u `AppDelegate.flutterEngine`. +class SceneDelegate: FlutterSceneDelegate { + override func scene( + _ scene: UIScene, + willConnectTo session: UISceneSession, + options connectionOptions: UIScene.ConnectionOptions + ) { + guard let windowScene = scene as? UIWindowScene, + let appDelegate = UIApplication.shared.delegate as? AppDelegate + else { return } + + let window = UIWindow(windowScene: windowScene) + window.rootViewController = FlutterViewController( + engine: appDelegate.flutterEngine, nibName: nil, bundle: nil) + self.window = window + window.makeKeyAndVisible() + + // Super podle okna scény přihlásí engine k událostem scény pro pluginy. + // Musí běžet dřív, než okno dostane i delegát aplikace: našel-li by ho + // tam, Flutter by ho bral jako starý způsob startu a přesunul do nového okna. + super.scene(scene, willConnectTo: session, options: connectionOptions) + + // Pluginy, které okno hledají přes delegáta aplikace (local_auth_darwin + // u dialogu „biometrie není nastavená“), by ho jinak nenašly. + appDelegate.window = window + } +} diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 01e0752..d4d441e 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -1,7 +1,7 @@ name: sepot description: "Šepot — Soukromý messenger s E2E šifrováním" publish_to: 'none' -version: 1.2.3+26 +version: 1.2.3+27 environment: sdk: ^3.10.8