UISceneDelegate adoption
Summary
#Apple now requires iOS developers to adopt the UIScene life cycle. This migration has implications on the app launch sequence and app life cycle.
Background
#During WWDC25, Apple announced the following:
In the release following iOS 26, any UIKit app built with the latest SDK will be required to use the UIScene life cycle, otherwise it will not launch.
To use the UIScene lifecycle with Flutter, migrate the following support:
- All Flutter apps that support iOS - See the migration guide for Flutter apps
- Flutter plugins that use iOS application lifecycle events - See the migration guide for plugins
- Flutter embedded in iOS native apps - See the migration guide for adding Flutter to an existing app
Migrating to UIScene shifts the AppDelegate's role—the UI lifecycle is now handled by the UISceneDelegate. The AppDelegate remains responsible for process events and the overall application lifecycle. All UI-related logic should be moved from the AppDelegate to the corresponding UISceneDelegate methods. After migrating to UIScene, UIKit won't call AppDelegate methods related to UI state.
Migration guide for Flutter apps
#The Flutter CLI will automatically migrate your app when you run flutter run
or flutter build ios
if your AppDelegate has not been customized. Otherwise,
you must migrate manually.
Migrate AppDelegate
#Previously, Flutter plugins were registered in
application:didFinishLaunchingWithOptions:
. To accomodate the new app launch
sequence, plugin registration must now be handled in a new callback called
didInitializeImplicitFlutterEngine
.
- Add
FlutterImplicitEngineDelegate
and moveGeneratedPluginRegistrant
.
@objc class AppDelegate: FlutterAppDelegate {
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
}
@interface AppDelegate : FlutterAppDelegate
@interface AppDelegate : FlutterAppDelegate <FlutterImplicitEngineDelegate>
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[GeneratedPluginRegistrant registerWithRegistry:self];
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
- (void)didInitializeImplicitFlutterEngine:(NSObject<FlutterImplicitEngineBridge>*)engineBridge {
[GeneratedPluginRegistrant registerWithRegistry:engineBridge.pluginRegistry];
}
- Create method channels and platform views in
didInitializeImplicitFlutterEngine
, if applicable.
If you previously created method channels or
platform views in
application:didFinishLaunchingWithOptions:
,
move that logic to didInitializeImplicitFlutterEngine
.
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
// Register plugins with `engineBridge.pluginRegistry`
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
// Create method channels with `engineBridge.applicationRegistrar.messenger()`
let batteryChannel = FlutterMethodChannel(
name: "samples.flutter.dev/battery",
binaryMessenger: engineBridge.applicationRegistrar.messenger()
)
// Create platform views with `engineBridge.applicationRegistrar.messenger()`
let factory = FLNativeViewFactory(messenger: engineBridge.applicationRegistrar.messenger())
}
func didInitializeImplicitFlutterEngine:(NSObject<FlutterImplicitEngineBridge>*)engineBridge {
// Register plugins with `engineBridge.pluginRegistry`
[GeneratedPluginRegistrant registerWithRegistry:engineBridge.pluginRegistry];
// Create method channels with `engineBridge.applicationRegistrar.messenger`
FlutterMethodChannel* batteryChannel = [FlutterMethodChannel
methodChannelWithName:@"samples.flutter.dev/battery"
binaryMessenger:engineBridge.applicationRegistrar.messenger];
// Create platform views with `engineBridge.applicationRegistrar.messenger`
FLNativeViewFactory* factory =
[[FLNativeViewFactory alloc] initWithMessenger:engineBridge.applicationRegistrar.messenger];
}
- Migrate any custom logic within application life cycle events.
Apple has deprecated application life cycle events related to UI state. After migrating to UIScene lifecycle, UIKit will no longer call these events.
If you were using one of these depreacted APIs, such as
applicationDidBecomeActive
,
you will likely need to create a SceneDelegate and migrate to scene life cycle
events. See Apple's
documenation
on migrating.
If you implement your own SceneDelegate, you must subclass it with
FlutterSceneDelegate
or conform to the FlutterSceneLifeCycleProvider
protocol. See the following
examples.
Migrate Info.plist
#To complete the migration to the UIScene lifecycle, add an Application Scene Manifest
to your Info.plist.
As seen in Xcode's editor:
As XML:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneDelegateClassName</key>
<string>FlutterSceneDelegate</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
</dict>
Migration guide for Flutter plugins
#Not all plugins use lifecycle events. If your plugin does, though, you will need to migrate to UIKit's scene-based lifecycle.
- Adopt the
FlutterSceneLifeCycleDelegate
protocol
public final class MyPlugin: NSObject, FlutterPlugin {
public final class MyPlugin: NSObject, FlutterPlugin, FlutterSceneLifeCycleDelegate {
@interface MyPlugin : NSObject<FlutterPlugin>
@interface MyPlugin : NSObject<FlutterPlugin, FlutterSceneLifeCycleDelegate>
- Registers the plugin as a receiver of
UISceneDelegate
calls.
To continue supporting apps that have not migrated to the UIScene lifecycle yet, you might consider remaining registered to the App Delegate and keeping the App Delegate events as well.
public static func register(with registrar: FlutterPluginRegistrar) {
...
registrar.addApplicationDelegate(instance)
registrar.addSceneDelegate(instance)
}
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
...
[registrar addApplicationDelegate:instance];
[registrar addSceneDelegate:instance];
}
- Add one or more of the following scene events that are needed for your plugin.
Most App Delegate UI events have a 1-to-1 replacement. To see details on each event, visit Apple's documentation on UISceneDelegate and UIWindowSceneDelegate.
public func scene(
_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions?
) -> Bool { }
public func sceneDidDisconnect(_ scene: UIScene) { }
public func sceneWillEnterForeground(_ scene: UIScene) { }
public func sceneDidBecomeActive(_ scene: UIScene) { }
public func sceneWillResignActive(_ scene: UIScene) { }
public func sceneDidEnterBackground(_ scene: UIScene) { }
public func scene(
_ scene: UIScene,
openURLContexts URLContexts: Set<UIOpenURLContext>
) -> Bool { }
public func scene(_ scene: UIScene, continue userActivity: NSUserActivity)
-> Bool { }
public func windowScene(
_ windowScene: UIWindowScene,
performActionFor shortcutItem: UIApplicationShortcutItem,
completionHandler: @escaping (Bool) -> Void
) -> Bool { }
- (BOOL)scene:(UIScene*)scene
willConnectToSession:(UISceneSession*)session
options:(nullable UISceneConnectionOptions*)connectionOptions;
- (void)sceneDidDisconnect:(UIScene*)scene { }
- (void)sceneWillEnterForeground:(UIScene*)scene { }
- (void)sceneDidBecomeActive:(UIScene*)scene { }
- (void)sceneWillResignActive:(UIScene*)scene { }
- (void)sceneDidEnterBackground:(UIScene*)scene { }
- (BOOL)scene:(UIScene*)scene openURLContexts:(NSSet<UIOpenURLContext*>*)URLContexts { }
- (BOOL)scene:(UIScene*)scene continueUserActivity:(NSUserActivity*)userActivity { }
- (BOOL)windowScene:(UIWindowScene*)windowScene
performActionForShortcutItem:(UIApplicationShortcutItem*)shortcutItem
completionHandler:(void (^)(BOOL succeeded))completionHandler { }
- Move launch logic from
application:willFinishLaunchingWithOptions:
andapplication:didFinishLaunchingWithOptions:
toscene:willConnectToSession:options:
.
Despite application:willFinishLaunchingWithOptions:
and
application:didFinishLaunchingWithOptions:
not being deprecated, after
migrating to UIScene lifecycle, the launch options will be nil
. Any logic
performed here related to the launch options should be moved to the
scene:willConnectToSession:options:
event.
Migration guide for adding Flutter to existing app (Add to App)
#Similar to the FlutterAppDelegate
, the FlutterSceneDelgate
is recommended
but not required. The FlutterSceneDelgate
forwards scene callbacks, such as
openURL
to plugins such as local_auth.
Create/Update a SceneDelegate (UIKit)
#import UIKit
import Flutter
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
class SceneDelegate: FlutterSceneDelegate {
@interface SceneDelegate : UIResponder <UIWindowSceneDelegate>
@interface SceneDelegate : FlutterSceneDelegate
Create/Update a SceneDelegate (SwiftUI)
#When using Flutter in a SwifUI app, you can optionally use a FlutterAppDelegate to receive application events. To migrate that to use UIScene events, you can make the following changes:
- Set the Scene Delegate to
FlutterSceneDelegate
inapplication:configurationForConnecting:options:
.
@Observable
class AppDelegate: FlutterAppDelegate {
...
override func application(
_ application: UIApplication,
configurationForConnecting connectingSceneSession: UISceneSession,
options: UIScene.ConnectionOptions
) -> UISceneConfiguration {
let configuration = UISceneConfiguration(
name: nil,
sessionRole: connectingSceneSession.role
)
configuration.delegateClass = FlutterSceneDelegate.self
return configuration
}
}
- If your app does not support multiple scenes, set
Enable Multiple Scenes
toNO
underApplication Scene Manifest
in your target's Info properties. This is enabled by default for SwiftUI apps.
Otherwise, see If your app supports multiple scenes for further instructions.
If you can't directly make FlutterSceneDelegate a subclass
#If you can't directly make FlutterSceneDelegate
a subclass, you can use the
FlutterSceneLifeCycleProvider
protocol and
FlutterPluginSceneLifeCycleDelegate
object to forward scene life cycle events
to Flutter.
import Flutter
import UIKit
class SceneDelegate: UIResponder, UIWindowSceneDelegate
class SceneDelegate: UIResponder, UIWindowSceneDelegate, FlutterSceneLifeCycleProvider
{
var sceneLifeCycleDelegate: FlutterPluginSceneLifeCycleDelegate =
FlutterPluginSceneLifeCycleDelegate()
var window: UIWindow?
func scene(
_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions
) {
sceneLifeCycleDelegate.scene(
scene,
willConnectTo: session,
options: connectionOptions
)
}
func sceneDidDisconnect(_ scene: UIScene) {
sceneLifeCycleDelegate.sceneDidDisconnect(scene)
}
func sceneWillEnterForeground(_ scene: UIScene) {
sceneLifeCycleDelegate.sceneWillEnterForeground(scene)
}
func sceneDidBecomeActive(_ scene: UIScene) {
sceneLifeCycleDelegate.sceneDidBecomeActive(scene)
}
func sceneWillResignActive(_ scene: UIScene) {
sceneLifeCycleDelegate.sceneWillResignActive(scene)
}
func sceneDidEnterBackground(_ scene: UIScene) {
sceneLifeCycleDelegate.sceneDidEnterBackground(scene)
}
func scene(
_ scene: UIScene,
openURLContexts URLContexts: Set<UIOpenURLContext>
) {
sceneLifeCycleDelegate.scene(scene, openURLContexts: URLContexts)
}
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
sceneLifeCycleDelegate.scene(scene, continue: userActivity)
}
func windowScene(
_ windowScene: UIWindowScene,
performActionFor shortcutItem: UIApplicationShortcutItem,
completionHandler: @escaping (Bool) -> Void
) {
sceneLifeCycleDelegate.windowScene(
windowScene,
performActionFor: shortcutItem,
completionHandler: completionHandler
)
}
}
@interface SceneDelegate : UIResponder <UIWindowSceneDelegate>
@interface SceneDelegate : UIResponder <UIWindowSceneDelegate, FlutterSceneLifeCycleProvider>
@property(strong, nonatomic) UIWindow* window;
@property (nonatomic,strong) FlutterPluginSceneLifeCycleDelegate *sceneLifeCycleDelegate;
@end
@implementation SceneDelegate
- (instancetype)init {
if (self = [super init]) {
_sceneLifeCycleDelegate = [[FlutterPluginSceneLifeCycleDelegate alloc] init];
}
return self;
}
- (void)scene:(UIScene*)scene
willConnectToSession:(UISceneSession*)session
options:(UISceneConnectionOptions*)connectionOptions {
[self.sceneLifeCycleDelegate scene:scene willConnectToSession:session options:connectionOptions];
}
- (void)sceneDidDisconnect:(UIScene*)scene {
[self.sceneLifeCycleDelegate sceneDidDisconnect:scene];
}
- (void)sceneDidBecomeActive:(UIScene*)scene {
[self.sceneLifeCycleDelegate sceneDidBecomeActive:scene];
}
- (void)sceneWillResignActive:(UIScene*)scene {
[self.sceneLifeCycleDelegate sceneWillResignActive:scene];
}
- (void)sceneWillEnterForeground:(UIScene*)scene {
[self.sceneLifeCycleDelegate sceneWillEnterForeground:scene];
}
- (void)sceneDidEnterBackground:(UIScene*)scene {
[self.sceneLifeCycleDelegate sceneDidEnterBackground:scene];
}
- (void)scene:(UIScene *)scene openURLContexts:(NSSet<UIOpenURLContext *> *)URLContexts {
[self.sceneLifeCycleDelegate scene:scene openURLContexts:URLContexts];
}
- (void)scene:(UIScene *)scene continueUserActivity:(NSUserActivity *)userActivity {
[self.sceneLifeCycleDelegate scene:scene continueUserActivity:userActivity];
}
- (void)windowScene:(UIWindowScene *)windowScene performActionForShortcutItem:(UIApplicationShortcutItem *)shortcutItem completionHandler:(void (^)(BOOL))completionHandler {
[self.sceneLifeCycleDelegate windowScene:windowScene performActionForShortcutItem:shortcutItem completionHandler:completionHandler];
}
If your app supports multiple scenes
#When multiple scenes is enabled (UIApplicationSupportsMultipleScenes), Flutter cannot automatically associate a
FlutterEngine
with a scene during the scene connection phase. In order for
plugins to receive launch connection information, the FlutterEngine
must be
manually registered with either the FlutterSceneDelegate
or
FlutterPluginSceneLifeCycleDelegate
during
scene:willConnectToSession:options:
. Otherwise, once the view, created by the
FlutterViewController
and FlutterEngine
, is added to the view heirarchy,
the FlutterEngine
will automatically register for scene events.
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)
}
}
#import <UIKit/UIKit.h>
#import <Flutter/Flutter.h>
#import <FlutterPluginRegistrant/GeneratedPluginRegistrant.h>
@interface SceneDelegate : FlutterSceneDelegate
@property (nonatomic, strong) FlutterEngine *flutterEngine;
@end
#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.
// If using FlutterSceneDelegate:
self.unregisterSceneLifeCycle(with: flutterEngine)
// If using FlutterSceneLifeCycleProvider:
sceneLifeCycleDelegate.unregisterSceneLifeCycle(with: flutterEngine)
// If using FlutterSceneDelegate:
[self unregisterSceneLifeCycleWithFlutterEngine:self.flutterEngine];
// If using FlutterSceneLifeCycleProvider:
[self.sceneLifeCycleDelegate unregisterSceneLifeCycleWithFlutterEngine:self.flutterEngine];
Bespoke FlutterViewController usage
#For apps that use a FlutterViewController
instantiated from Storyboards in
application:didFinishLaunchingWithOptions:
for reasons other than
creating platform channels, it is their responsibility to
accommodate the new initialization order.
Migration options:
- Subclass
FlutterViewController
and put the logic in the subclasses'awakeFromNib
. - Specify a
UISceneDelegate
in theInfo.plist
or in theUIApplicationDelegate
and put the logic inscene:willConnectToSession:options:
. For more information, check out Apple's documentation.
Example
#@objc class MyViewController: FlutterViewController {
override func awakeFromNib() {
self.awakeFromNib()
doSomethingWithFlutterViewController(self)
}
}
Hide Migration Warning
#To hide the Flutter CLI warning about migrating to UIScene, add the following to your pubspec.yaml:
flutter:
config:
enable-uiscene-migration: false
Timeline
#- Landed in main: TBD
- Landed in stable: TBD
- Unknown: Apple changes their warning to an assert and Flutter apps that
haven't adopted
UISceneDelegate
will start crashing on startup with the latest SDK.
References
#- Issue 167267 - The initial reported issue.
除非另有说明,本文档之所提及适用于 Flutter 的最新稳定版本,本页面最后更新时间: 2025-10-07。 查看文档源码 或者 为本页面内容提出建议.