From fdd6fd674ef8fb7c2bda9a188199cadeaeed0374 Mon Sep 17 00:00:00 2001 From: Huan Lin Date: Tue, 7 Jul 2026 11:47:22 -0700 Subject: [PATCH 1/6] [ios]update uiscene adoption --- .../breaking-changes/uiscenedelegate.md | 263 ++++++++---------- 1 file changed, 121 insertions(+), 142 deletions(-) diff --git a/sites/docs/src/content/release/breaking-changes/uiscenedelegate.md b/sites/docs/src/content/release/breaking-changes/uiscenedelegate.md index 39e5f629b8e..2f367df4745 100644 --- a/sites/docs/src/content/release/breaking-changes/uiscenedelegate.md +++ b/sites/docs/src/content/release/breaking-changes/uiscenedelegate.md @@ -1,8 +1,8 @@ --- -title: UISceneDelegate adoption +title: UIScene adoption description: >- Learn how to migrate your Flutter iOS app, add-to-app integration, or plugin - to Apple's required UIScene lifecycle using FlutterSceneDelegate. + to Apple's required UIScene lifecycle. --- {% render "docs/breaking-changes.md" %} @@ -595,143 +595,8 @@ forward scene lifecycle events to Flutter. ### If your app supports multiple scenes -When multiple scenes are enabled (`UIApplicationSupportsMultipleScenes`), -Flutter can't automatically connect a `FlutterEngine` to its -corresponding `UIScene` during the initial scene connection phase. - -To ensure that Flutter plugins can receive the initial scene setup options -(such as deep link URLs or shortcut items passed inside the -`UIScene.ConnectionOptions` payload), you must manually register the -`FlutterEngine` with either your `FlutterSceneDelegate` or your -`FlutterPluginSceneLifeCycleDelegate` inside the -`scene:willConnectToSession:options:` method. - -If you don't perform this manual registration, -the `FlutterEngine` still registers itself automatically once the view -created by the `FlutterViewController` is added to the active view hierarchy. -However, by that point, the engine and its plugins have already missed -any launch connection events passed during `willConnectToSession:`. - - - - -```swift title="SceneDelegate.swift" -import Flutter -import FlutterPluginRegistrant -import UIKit - -class SceneDelegate: FlutterSceneDelegate { - let flutterEngine = FlutterEngine(name: "my flutter engine") - - override func scene( - _ scene: UIScene, - willConnectTo session: UISceneSession, - options connectionOptions: UIScene.ConnectionOptions - ) { - guard let windowScene = scene as? UIWindowScene else { return } - window = UIWindow(windowScene: windowScene) - - flutterEngine.run() - GeneratedPluginRegistrant.register(with: flutterEngine) - - // If using FlutterSceneDelegate: - self.registerSceneLifeCycle(with: flutterEngine) - - // If using FlutterSceneLifeCycleProvider: - // sceneLifeCycleDelegate.registerSceneLifeCycle(with: flutterEngine) - - let viewController = ViewController(engine: flutterEngine) - window?.rootViewController = viewController - window?.makeKeyAndVisible() - super.scene(scene, willConnectTo: session, options: connectionOptions) - } -} -``` - - - - -```objc title="SceneDelegate.h" -#import -#import -#import - -@interface SceneDelegate : FlutterSceneDelegate -@property(nonatomic, strong) FlutterEngine *flutterEngine; -@end -``` - -```objc title="SceneDelegate.m" -#import "SceneDelegate.h" -#import "ViewController.h" - -@implementation SceneDelegate - -- (instancetype)init { - if (self = [super init]) { - _flutterEngine = [[FlutterEngine alloc] initWithName:@"my flutter engine"]; - } - return self; -} - -- (void)scene:(UIScene *)scene - willConnectToSession:(UISceneSession *)session - options:(UISceneConnectionOptions *)connectionOptions { - if (![scene isKindOfClass:[UIWindowScene class]]) { - return; - } - UIWindowScene *windowScene = (UIWindowScene *)scene; - self.window = [[UIWindow alloc] initWithWindowScene:windowScene]; - - [self.flutterEngine run]; - [GeneratedPluginRegistrant registerWithRegistry:self.flutterEngine]; - - // If using FlutterSceneDelegate: - [self registerSceneLifeCycleWithFlutterEngine:self.flutterEngine]; - - // If using FlutterSceneLifeCycleProvider: - // [self.sceneLifeCycleDelegate registerSceneLifeCycleWithFlutterEngine:self.flutterEngine]; - - ViewController *viewController = [[ViewController alloc] initWithEngine:self.flutterEngine]; - self.window.rootViewController = viewController; - [self.window makeKeyAndVisible]; - [super scene:scene willConnectToSession:session options:connectionOptions]; -} - -@end -``` - - - - -If you manually register a `FlutterEngine` with a scene, -you must also unregister it if the view -created by the `FlutterEngine` changes scenes. - - - - -```swift -// If using FlutterSceneDelegate: -self.unregisterSceneLifeCycle(with: flutterEngine) - -// If using FlutterSceneLifeCycleProvider: -sceneLifeCycleDelegate.unregisterSceneLifeCycle(with: flutterEngine) -``` - - - - -```objc -// If using FlutterSceneDelegate: -[self unregisterSceneLifeCycleWithFlutterEngine:self.flutterEngine]; - -// If using FlutterSceneLifeCycleProvider: -[self.sceneLifeCycleDelegate unregisterSceneLifeCycleWithFlutterEngine:self.flutterEngine]; -``` - - - +Flutter doesn't fully support multiple scenes yet, +but support is in progress. @@ -821,7 +686,6 @@ migrate it to UIKit's scene-based lifecycle as follows: | [`application:continueUserActivity:restorationHandler:`][] | [`scene:continueUserActivity:`][] | | [`application:performActionForShortcutItem:completionHandler:`][] | [`windowScene:performActionForShortcutItem:completionHandler:`][] | | [`application:openURL:options:`][] | [`scene:openURLContexts:`][] | - | [`application:performFetchWithCompletionHandler:`][] | [`BGAppRefreshTask`][] | | [`application:willFinishLaunchingWithOptions:`][] | [`scene:willConnectToSession:options:`][] | | [`application:didFinishLaunchingWithOptions:`][] | [`scene:willConnectToSession:options:`][] | @@ -911,6 +775,120 @@ migrate it to UIKit's scene-based lifecycle as follows: Move any logic that relies on the launch options to the `scene:willConnectToSession:options:` event. + For apps that adopt `UIScene`, Flutter calls + `application:willFinishLaunchingWithOptions:` and + `application:didFinishLaunchingWithOptions:` during the + `scene:willConnectToSession:options:` callback. + However, some Apple APIs must be called before + `application:didFinishLaunchingWithOptions:` returns. + To work around this restriction, plugins can expose an API that + lets you register background tasks directly in the `AppDelegate`'s + `application:willFinishLaunchingWithOptions:` method. + + The following Apple APIs must be configured before app launch finishes: + + - [`BGTaskScheduler.registerForTaskWithIdentifier:usingQueue:launchHandler:`][] + + > Registration of all launch handlers must be complete + > before the end of `applicationDidFinishLaunching:`. + + - [`UNUserNotificationCenterDelegate`][] + + > You must assign your delegate object to the `UNUserNotificationCenter` + > object before your app finishes launching. + > For example, in an iOS app, you must assign it in the + > `application:willFinishLaunchingWithOptions:` or + > `application:didFinishLaunchingWithOptions:` + > method of your app delegate. + > Assigning a delegate after the system calls these methods + > might cause you to miss incoming notifications. + + - [`HKHealthStore.enableBackgroundDeliveryForType:frequency:withCompletion:`][] + + > If you plan on supporting background delivery, + > set up all your observer queries in your app delegate’s + > `application:didFinishLaunchingWithOptions:` method. + > By setting up the queries in + > `application:didFinishLaunchingWithOptions:`, + > you ensure that you’ve instantiated your queries, + > and they’re ready to use before HealthKit delivers the updates. + + For example, to support `BGTaskScheduler`: + + + + ```swift diff + public class BGTaskPlugin: NSObject, FlutterPlugin { + public static let shared = BGTaskPlugin() + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = ... + registrar.addMethodCallDelegate(shared, channel: channel) + registrar.addApplicationDelegate(shared) + } + + // A new API for apps to call in their `AppDelegate`'s `application:didFinishLaunchingWithOptions:` + + public func registerBackgroundHandler(identifer: String) { + + BGTaskScheduler.register(forTaskWithIdentifier: identifier, using: .main) { task in + + // Handle background task execution + + } + + } + - public func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [AnyHashable : Any] = [:]) -> Bool { + - BGTaskScheduler.register(forTaskWithIdentifier: identifier, using: .main) { task in + - // Handle background task execution + - } + - return true + - } + } + ``` + + + + + ```objc diff + @interface BGTaskPlugin : NSObject + + + (instancetype)sharedInstance; + + - (void)registerBackgroundHandlerWithIdentifier:(NSString *)identifier; + @end + + @implementation BGTaskPlugin + + + + (instancetype)sharedInstance { + + static BGTaskPlugin *sharedInstance = nil; + + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + + sharedInstance = [[BGTaskPlugin alloc] init]; + + }); + + return sharedInstance; + + } + + + (void)registerWithRegistrar:(NSObject *)registrar { + FlutterMethodChannel *channel = ... + [registrar addMethodCallDelegate:[BGTaskPlugin sharedInstance] channel:channel]; + [registrar addApplicationDelegate:[BGTaskPlugin sharedInstance]]; + } + + + // A new API for apps to call in their `AppDelegate`'s `application:didFinishLaunchingWithOptions:` + + - (void)registerBackgroundHandlerWithIdentifier:(NSString *)identifier { + + [[BGTaskScheduler sharedScheduler] registerForTaskWithIdentifier:identifier + + usingQueue:dispatch_get_main_queue() + + launchHandler:^(BGTask *task) { + + // Handle background task execution + + }]; + + } + + - - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + - [[BGTaskScheduler sharedScheduler] registerForTaskWithIdentifier:identifier + - usingQueue:dispatch_get_main_queue() + - launchHandler:^(BGTask *task) { + - // Handle background task execution + - }]; + - return YES; + - } + @end + ``` + + + + 1. Optional: Migrate other deprecated APIs to support multiple scenes in the future. @@ -1022,7 +1000,6 @@ migrate it to UIKit's scene-based lifecycle as follows: [`application:continueUserActivity:restorationHandler:`]: {{site.apple-dev}}/documentation/uikit/uiapplicationdelegate/1623072-application [`application:performActionForShortcutItem:completionHandler:`]: {{site.apple-dev}}/documentation/uikit/uiapplicationdelegate/application(_:performactionfor:completionhandler:) [`application:openURL:options:`]: {{site.apple-dev}}/documentation/uikit/uiapplicationdelegate/1623112-application -[`application:performFetchWithCompletionHandler:`]: {{site.apple-dev}}/documentation/uikit/uiapplicationdelegate/1623125-application [`application:willFinishLaunchingWithOptions:`]: {{site.apple-dev}}/documentation/uikit/uiapplicationdelegate/1623032-application [`application:didFinishLaunchingWithOptions:`]: {{site.apple-dev}}/documentation/uikit/uiapplicationdelegate/1622921-application [`sceneDidBecomeActive`]: {{site.apple-dev}}/documentation/uikit/uiscenedelegate/3197915-scenedidbecomeactive @@ -1032,7 +1009,6 @@ migrate it to UIKit's scene-based lifecycle as follows: [`scene:continueUserActivity:`]: {{site.apple-dev}}/documentation/uikit/uiscenedelegate/scene(_:continue:) [`windowScene:performActionForShortcutItem:completionHandler:`]: {{site.apple-dev}}/documentation/uikit/uiwindowscenedelegate/3238088-windowscene [`scene:openURLContexts:`]: {{site.apple-dev}}/documentation/uikit/uiscenedelegate/3238059-scene -[`BGAppRefreshTask`]: {{site.apple-dev}}/documentation/backgroundtasks/bgapprefreshtask [`scene:willConnectToSession:options:`]: {{site.apple-dev}}/documentation/uikit/uiscenedelegate/3197914-scene [`UIScreen mainScreen`]: {{site.apple-dev}}/documentation/uikit/uiscreen/1617815-mainscreen [`UIWindowScene screen`]: {{site.apple-dev}}/documentation/uikit/uiwindowscene/screen?language=objc @@ -1042,6 +1018,9 @@ migrate it to UIKit's scene-based lifecycle as follows: [`UIWindowScene windows`]: {{site.apple-dev}}/documentation/uikit/uiwindowscene/windows?language=objc [`UIApplicationDelegate window`]: {{site.apple-dev}}/documentation/uikit/uiapplicationdelegate/window [`UIView window`]: {{site.apple-dev}}/documentation/uikit/uiview/window?language=objc +[`BGTaskScheduler.registerForTaskWithIdentifier:usingQueue:launchHandler:`]: <{{site.apple-dev}}/documentation/backgroundtasks/bgtaskscheduler/register(fortaskwithidentifier:using:launchhandler:)?language=objc> +[`UNUserNotificationCenterDelegate`]: {{site.apple-dev}}/documentation/usernotifications/unusernotificationcenterdelegate?language=objc +[`HKHealthStore.enableBackgroundDeliveryForType:frequency:withCompletion:`]: <{{site.apple-dev}}/documentation/healthkit/hkhealthstore/enablebackgrounddelivery(for:frequency:withcompletion:)?language=objc> ## Bespoke FlutterViewController usage From fa72f45d92e7feb00e8a2bb6ba33d708955d121b Mon Sep 17 00:00:00 2001 From: Huan Lin Date: Tue, 14 Jul 2026 14:29:23 -0700 Subject: [PATCH 2/6] gemini comment --- .../src/content/release/breaking-changes/uiscenedelegate.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sites/docs/src/content/release/breaking-changes/uiscenedelegate.md b/sites/docs/src/content/release/breaking-changes/uiscenedelegate.md index 2f367df4745..518387fb7aa 100644 --- a/sites/docs/src/content/release/breaking-changes/uiscenedelegate.md +++ b/sites/docs/src/content/release/breaking-changes/uiscenedelegate.md @@ -783,7 +783,7 @@ migrate it to UIKit's scene-based lifecycle as follows: `application:didFinishLaunchingWithOptions:` returns. To work around this restriction, plugins can expose an API that lets you register background tasks directly in the `AppDelegate`'s - `application:willFinishLaunchingWithOptions:` method. + `application:didFinishLaunchingWithOptions:` method. The following Apple APIs must be configured before app launch finishes: @@ -826,7 +826,7 @@ migrate it to UIKit's scene-based lifecycle as follows: registrar.addApplicationDelegate(shared) } + // A new API for apps to call in their `AppDelegate`'s `application:didFinishLaunchingWithOptions:` - + public func registerBackgroundHandler(identifer: String) { + + public func registerBackgroundHandler(identifier: String) { + BGTaskScheduler.register(forTaskWithIdentifier: identifier, using: .main) { task in + // Handle background task execution + } From c57b27f21f10c408f68ea751366557f2db9c702b Mon Sep 17 00:00:00 2001 From: Huan Lin Date: Fri, 17 Jul 2026 10:44:49 -0700 Subject: [PATCH 3/6] fix broken tab (missing blank line) --- .../docs/src/content/release/breaking-changes/uiscenedelegate.md | 1 + 1 file changed, 1 insertion(+) diff --git a/sites/docs/src/content/release/breaking-changes/uiscenedelegate.md b/sites/docs/src/content/release/breaking-changes/uiscenedelegate.md index 518387fb7aa..5b824334414 100644 --- a/sites/docs/src/content/release/breaking-changes/uiscenedelegate.md +++ b/sites/docs/src/content/release/breaking-changes/uiscenedelegate.md @@ -814,6 +814,7 @@ migrate it to UIKit's scene-based lifecycle as follows: > and they’re ready to use before HealthKit delivers the updates. For example, to support `BGTaskScheduler`: + From 34f1ffbbfd4f89017f51f9cbcd0dab87d97f8d97 Mon Sep 17 00:00:00 2001 From: Huan Lin Date: Fri, 17 Jul 2026 11:01:23 -0700 Subject: [PATCH 4/6] address comments --- .../breaking-changes/uiscenedelegate.md | 73 ++++++++++--------- 1 file changed, 39 insertions(+), 34 deletions(-) diff --git a/sites/docs/src/content/release/breaking-changes/uiscenedelegate.md b/sites/docs/src/content/release/breaking-changes/uiscenedelegate.md index 5b824334414..36c81a3369a 100644 --- a/sites/docs/src/content/release/breaking-changes/uiscenedelegate.md +++ b/sites/docs/src/content/release/breaking-changes/uiscenedelegate.md @@ -775,50 +775,25 @@ migrate it to UIKit's scene-based lifecycle as follows: Move any logic that relies on the launch options to the `scene:willConnectToSession:options:` event. - For apps that adopt `UIScene`, Flutter calls - `application:willFinishLaunchingWithOptions:` and - `application:didFinishLaunchingWithOptions:` during the - `scene:willConnectToSession:options:` callback. - However, some Apple APIs must be called before - `application:didFinishLaunchingWithOptions:` returns. - To work around this restriction, plugins can expose an API that - lets you register background tasks directly in the `AppDelegate`'s - `application:didFinishLaunchingWithOptions:` method. + 1. If your plugin uses an API that must be called before + `application:didFinishLaunchingWithOptions:` completes, + you should expose a public method that app developers + can call directly in their AppDelegate. The following Apple APIs must be configured before app launch finishes: - [`BGTaskScheduler.registerForTaskWithIdentifier:usingQueue:launchHandler:`][] - - > Registration of all launch handlers must be complete - > before the end of `applicationDidFinishLaunching:`. - - [`UNUserNotificationCenterDelegate`][] - - > You must assign your delegate object to the `UNUserNotificationCenter` - > object before your app finishes launching. - > For example, in an iOS app, you must assign it in the - > `application:willFinishLaunchingWithOptions:` or - > `application:didFinishLaunchingWithOptions:` - > method of your app delegate. - > Assigning a delegate after the system calls these methods - > might cause you to miss incoming notifications. - - [`HKHealthStore.enableBackgroundDeliveryForType:frequency:withCompletion:`][] - > If you plan on supporting background delivery, - > set up all your observer queries in your app delegate’s - > `application:didFinishLaunchingWithOptions:` method. - > By setting up the queries in - > `application:didFinishLaunchingWithOptions:`, - > you ensure that you’ve instantiated your queries, - > and they’re ready to use before HealthKit delivers the updates. - For example, to support `BGTaskScheduler`: ```swift diff + // Plugin + public class BGTaskPlugin: NSObject, FlutterPlugin { public static let shared = BGTaskPlugin() public static func register(with registrar: FlutterPluginRegistrar) { @@ -837,14 +812,28 @@ migrate it to UIKit's scene-based lifecycle as follows: - // Handle background task execution - } - return true - - } - } + - } + } + + // App + + class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + + BGTaskPlugin.shared.registerBackgroundHandler(identifier: "com.example.task") + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } + } ``` ```objc diff + // Plugin + @interface BGTaskPlugin : NSObject + + (instancetype)sharedInstance; + - (void)registerBackgroundHandlerWithIdentifier:(NSString *)identifier; @@ -885,12 +874,28 @@ migrate it to UIKit's scene-based lifecycle as follows: - return YES; - } @end + + // App + + @implementation AppDelegate + - (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + + [[BGTaskPlugin sharedInstance] registerBackgroundHandlerWithIdentifier:@"com.example.task"]; + return [super application:application didFinishLaunchingWithOptions:launchOptions]; + } + @end ``` - 1. Optional: Migrate other deprecated APIs to support + This change is required due to UIScene changing the app launch + sequence. For apps that adopt `UIScene`, Flutter calls + `application:willFinishLaunchingWithOptions:` and + `application:didFinishLaunchingWithOptions:` during the + `scene:willConnectToSession:options:` callback. + + 1. Migrate other deprecated APIs to support multiple scenes in the future. | Deprecated API | UIScene replacement | From 9c4aeb16c0a02da8c76d01a78425feae7f6a7057 Mon Sep 17 00:00:00 2001 From: Huan Lin Date: Fri, 17 Jul 2026 11:40:47 -0700 Subject: [PATCH 5/6] call out nil vc and highlight registrar.vc API --- .../content/release/breaking-changes/uiscenedelegate.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/sites/docs/src/content/release/breaking-changes/uiscenedelegate.md b/sites/docs/src/content/release/breaking-changes/uiscenedelegate.md index 36c81a3369a..f4b8a5b8cf2 100644 --- a/sites/docs/src/content/release/breaking-changes/uiscenedelegate.md +++ b/sites/docs/src/content/release/breaking-changes/uiscenedelegate.md @@ -907,8 +907,11 @@ migrate it to UIKit's scene-based lifecycle as follows: {:.table} - Instead of accessing these APIs, - access the `windowScene` through the `viewController`, + Because `UIApplication.shared.delegate.window` is `nil` + after adopting `UIScene`, + you should access the `viewController` + via the plugin registrar's `viewController` property + and retrieve the window or `windowScene` from there, as shown in the following examples. From 95a58283ad97cc0562aa2cb80d3a71b444b83ff0 Mon Sep 17 00:00:00 2001 From: Huan Lin Date: Fri, 17 Jul 2026 11:50:25 -0700 Subject: [PATCH 6/6] remove multi scene mentions --- .../src/content/release/breaking-changes/uiscenedelegate.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sites/docs/src/content/release/breaking-changes/uiscenedelegate.md b/sites/docs/src/content/release/breaking-changes/uiscenedelegate.md index f4b8a5b8cf2..6bee42dec23 100644 --- a/sites/docs/src/content/release/breaking-changes/uiscenedelegate.md +++ b/sites/docs/src/content/release/breaking-changes/uiscenedelegate.md @@ -895,8 +895,8 @@ migrate it to UIKit's scene-based lifecycle as follows: `application:didFinishLaunchingWithOptions:` during the `scene:willConnectToSession:options:` callback. - 1. Migrate other deprecated APIs to support - multiple scenes in the future. + 1. Migrate other deprecated APIs to properly + access the `viewController`, `screen`, or `window`. | Deprecated API | UIScene replacement | |:-----------------------------------|:------------------------------|