From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id 1487A1FF140 for ; Fri, 10 Apr 2026 17:09:21 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 472EE20F9D; Fri, 10 Apr 2026 17:10:02 +0200 (CEST) From: Shan Shaji To: pve-devel@lists.proxmox.com Subject: [PATCH pve_flutter_frontend 3/4] fix: migrate to UIScene lifecycle for iOS 26+ compatibility Date: Fri, 10 Apr 2026 17:09:31 +0200 Message-ID: <20260410150935.25870-4-s.shaji@proxmox.com> X-Mailer: git-send-email 2.50.1 In-Reply-To: <20260410150935.25870-1-s.shaji@proxmox.com> References: <20260410150935.25870-1-s.shaji@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1775833720493 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.028 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment KAM_LOTSOFHASH 0.25 Emails with lots of hash-like gibberish SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: 7DAZLAIYPM7WUUVWE46SD6RGQ6ALGIVV X-Message-ID-Hash: 7DAZLAIYPM7WUUVWE46SD6RGQ6ALGIVV X-MailFrom: s.shaji@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: Apple has announced that starting with the SDK following iOS 26 [0], all UIKit-based apps must adopt the UIScene lifecycle. Apps that continue to rely solely on the legacy AppDelegate lifecycle for UI initialization will fail to launch. The plugin registration was happening inside the `application:didFinishLaunchingWithOptions` method. To accommodate the new app launch sequence moved the initialization to `didInitializeImplicitFlutterEngine` [1] This change also shifts window management away from the AppDelegate. Attempting to access the window object there may return nil, as the UI lifecycle is now handled by the new SceneDelegate. To fix the issue [2][3]: * Defined a new SceneDelegate class (subclassing FlutterSceneDelegate). * Registered the SceneDelegate within the Info.plist under the `UIApplicationSceneManifest`. * Migrated all file sharing logic and method channel setups from the `AppDelegate` to the `SceneDelegate` within the scene(_:willConnectTo:options:) method. - [0] https://docs.flutter.dev/release/breaking-changes/uiscenedelegate#background - [1] https://docs.flutter.dev/release/breaking-changes/uiscenedelegate#migrate-appdelegate - [2] https://docs.flutter.dev/release/breaking-changes/uiscenedelegate#bespoke-flutterviewcontroller-usage - [3] https://developer.apple.com/documentation/uikit/specifying-the-scenes-your-app-supports#Configure-the-details-for-each-scene Signed-off-by: Shan Shaji --- ios/Runner.xcodeproj/project.pbxproj | 4 ++ ios/Runner/AppDelegate.swift | 53 ++----------------------- ios/Runner/Info.plist | 21 ++++++++++ ios/Runner/SceneDelegate.swift | 59 ++++++++++++++++++++++++++++ 4 files changed, 88 insertions(+), 49 deletions(-) create mode 100644 ios/Runner/SceneDelegate.swift diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index a9d4fd5..8bf8472 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 1B87D59D2F89184000623BAC /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B87D59C2F89183C00623BAC /* SceneDelegate.swift */; }; 1BE02BA02EB5028D00B28B3B /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = 1BE02B9F2EB5028D00B28B3B /* AppIcon.icon */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; @@ -47,6 +48,7 @@ 022505048A677FEA7AF056D1 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 1B87D59C2F89183C00623BAC /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 1BE02B9F2EB5028D00B28B3B /* AppIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = AppIcon.icon; sourceTree = ""; }; 3158807C4D56CFC909080136 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; @@ -145,6 +147,7 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( + 1B87D59C2F89183C00623BAC /* SceneDelegate.swift */, 1BE02B9F2EB5028D00B28B3B /* AppIcon.icon */, 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, @@ -382,6 +385,7 @@ files = ( 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + 1B87D59D2F89184000623BAC /* SceneDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 428976f..bb1afce 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -2,60 +2,15 @@ import Flutter import UIKit @main -@objc class AppDelegate: FlutterAppDelegate { +@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { - let controller: FlutterViewController = window?.rootViewController as! FlutterViewController - let channel: FlutterMethodChannel = FlutterMethodChannel( - name: "com.proxmox.app.pve_flutter_frontend/filesharing", - binaryMessenger: controller.binaryMessenger) - - channel.setMethodCallHandler({ - [weak self] (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in - - guard call.method == "shareFile" else { - result(FlutterMethodNotImplemented) - return - } - - let arguments = call.arguments as? [String: Any] - let path = arguments?["path"] as? String - let type = arguments?["type"] as? String - - if let filePath = path, let _ = type { - self?.shareFile(atPath: filePath, from: controller, result: result) - } else { - result(FlutterError(code: "FileNotFoundException", message: "File not found", details: nil)) - } - }) - - GeneratedPluginRegistrant.register(with: self) - return super.application(application, didFinishLaunchingWithOptions: launchOptions) } - - private func shareFile(atPath path: String, from controller: UIViewController, result: @escaping FlutterResult) { - let fileURL = URL(fileURLWithPath: path) - let activityVC = UIActivityViewController( - activityItems: [fileURL], - applicationActivities: nil, - ) - - // To avoid crashing in iPad - if let popover = activityVC.popoverPresentationController { - popover.sourceView = controller.view - popover.sourceRect = CGRect( - x: controller.view.bounds.midX, - y: controller.view.bounds.midY, - width: 0, - height: 0, - ) - } - - controller.present(activityVC, animated: true) { - result(nil) - } + + func didInitializeImplicitFlutterEngine(_ engineBridge: any FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) } } diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index a5587ed..9ba026f 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -2,6 +2,27 @@ + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneConfigurationName + flutter + UISceneStoryboardFile + Main + + + + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName diff --git a/ios/Runner/SceneDelegate.swift b/ios/Runner/SceneDelegate.swift new file mode 100644 index 0000000..2ceca09 --- /dev/null +++ b/ios/Runner/SceneDelegate.swift @@ -0,0 +1,59 @@ +import Flutter +import UIKit + +class SceneDelegate: FlutterSceneDelegate { + override func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + super.scene(scene, willConnectTo: session, options: connectionOptions) + + guard let window = self.window, + let controller = window.rootViewController as? FlutterViewController else { + return + } + + let channel: FlutterMethodChannel = FlutterMethodChannel( + name: "com.proxmox.app.pve_flutter_frontend/filesharing", + binaryMessenger: controller.binaryMessenger) + + channel.setMethodCallHandler({ + [weak self] (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in + + guard call.method == "shareFile" else { + result(FlutterMethodNotImplemented) + return + } + + let arguments = call.arguments as? [String: Any] + let path = arguments?["path"] as? String + let type = arguments?["type"] as? String + + if let filePath = path, let _ = type { + self?.shareFile(atPath: filePath, from: controller, result: result) + } else { + result(FlutterError(code: "FileNotFoundException", message: "File not found", details: nil)) + } + }) + } + + private func shareFile(atPath path: String, from controller: UIViewController, result: @escaping FlutterResult) { + let fileURL = URL(fileURLWithPath: path) + let activityVC = UIActivityViewController( + activityItems: [fileURL], + applicationActivities: nil, + ) + + // To avoid crashing in iPad + if let popover = activityVC.popoverPresentationController { + popover.sourceView = controller.view + popover.sourceRect = CGRect( + x: controller.view.bounds.midX, + y: controller.view.bounds.midY, + width: 0, + height: 0, + ) + } + + controller.present(activityVC, animated: true) { + result(nil) + } + } +} -- 2.50.1