fix(ios): adopt the UIScene life cycle — 1.2.3 (27)

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.
This commit is contained in:
Julius Joska
2026-10-23 22:28:10 +02:00
parent 10ca35e352
commit 8780486eae
6 changed files with 85 additions and 27 deletions
+4
View File
@@ -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 = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
AA00000000000000000000B1 /* SceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
@@ -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;
+23 -24
View File
@@ -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)
+19 -2
View File
@@ -48,8 +48,25 @@
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
</dict>
</array>
</dict>
</dict>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
+32
View File
@@ -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
}
}