Migrating to the UIScene Lifecycle in Flutter
Xcode 27 requires the UIScene lifecycle, and Flutter 3.41+ migrates stock apps automatically. A hands-on guide for the rest: AppDelegate, Info.plist, plugins, and what to re-verify on the Dart side.
Published on • October 17, 2026
AI Assistant

Apple’s deadline is not a suggestion. Starting with Xcode 27 and the iOS 27 SDK, a UIKit app that has not adopted the UIScene lifecycle fails to launch on startup, and Flutter’s own breaking change documentation repeats the warning in its timeline section: apps that have not migrated “will crash on startup when built with Xcode 27.”
The good news is that most Flutter projects are already done. As of Flutter 3.41, UIScene support is the default and the Flutter CLI migrates any project with an unmodified AppDelegate automatically; the work first landed in Flutter 3.38. The bad news is that the projects which break are exactly the interesting ones: custom AppDelegates, add-to-app embeds, and plugins that hook application lifecycle events.
This is the guide for those projects. It covers what actually changed in the launch sequence, the four edits the manual migration needs, the plugin audit that catches silent failures, and what to re-verify from the Dart side once the native layer is done.
What actually changed
The migration splits one delegate into two jobs. The AppDelegate keeps process-level events and the overall application lifecycle; the new UISceneDelegate takes over everything UI. After you migrate, UIKit simply stops calling the AppDelegate methods related to UI state - applicationDidBecomeActive, applicationWillEnterForeground, and friends - so any logic parked there is now dead code, not slow code.
Three consequences have real blast radius in a Flutter project:
- The launch sequence shifts. Flutter registers plugins during
scene:willConnectToSession:options:, after UIKit’sapplication:didFinishLaunchingWithOptions:returns. Anything you assumed ran “at launch” may now run later. windowis nil earlier. Readingwindow?.rootViewControllerto grab theFlutterViewControllerinsidedidFinishLaunchingWithOptions:can crash; Flutter’s guide flags this pattern explicitly as BAD.- Launch options are nil.
application:willFinishLaunchingWithOptions:andapplication:didFinishLaunchingWithOptions:are not deprecated, but their launch options come through empty after migration. Logic that depended on them belongs inscene:willConnectToSession:options:.
Apple’s own material explains the model behind this: the app life cycle documentation describes the delegate split, and TN3187 walks through the migration event by event.
Step 0: find out where you stand
Run a build. If your AppDelegate is stock, the CLI does the migration for you:
$ flutter run -d ios
...
Finished migration to UIScene lifecycle
If the message appears and nothing in your AppDelegate touches UI lifecycle events, you are done - close the ticket. If the CLI warns instead, it prints instructions and you continue manually. The escape hatches (covered at the end) exist precisely so you can postpone this, but they are time-boxed by Apple’s release, not by your schedule.
Ask these three questions before editing anything:
- Does our
AppDelegatedo more than callsuperand register plugins? - Do we embed Flutter in an existing native app (add-to-app), or ship a SwiftUI host?
- Do any plugins we use register application lifecycle callbacks?
Step 1: move plugin and channel setup into the engine callback
Plugins used to be registered in application:didFinishLaunchingWithOptions:. The replacement is the FlutterImplicitEngineDelegate protocol:
// ios/Runner/AppDelegate.swift
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// GeneratedPluginRegistrant.register(with: self) no longer belongs here.
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
// Method channels and platform views move here as well.
let batteryChannel = FlutterMethodChannel(
name: "samples.flutter.dev/battery",
binaryMessenger: engineBridge.applicationRegistrar.messenger()
)
batteryChannel.setMethodCallHandler { _, result in
result(NSNumber(value: 100))
}
}
}
The Objective-C equivalent adopts the same protocol on the @interface and registers with engineBridge.pluginRegistry in -didInitializeImplicitFlutterEngine:. Two rules of thumb: channels and platform views created at launch move into this callback, and any attempt to reach the FlutterViewController through window at this stage is a crash you will only see on device.
Step 2: add the scene manifest
The Info.plist entry is what tells UIKit which delegate class to instantiate for a window scene. Xcode edits it under Application Scene Manifest; as XML:
<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>
FlutterSceneDelegate is Flutter’s own class and is enough for most apps. When you need your own - say to observe scene events natively - subclass it:
// ios/Runner/SceneDelegate.swift
import Flutter
import UIKit
class SceneDelegate: FlutterSceneDelegate {}
…and point UISceneDelegateClassName at $(PRODUCT_MODULE_NAME).SceneDelegate instead. If you cannot subclass FlutterSceneDelegate because you already inherit from something else, conform to FlutterSceneLifeCycleProvider, hold a FlutterPluginSceneLifeCycleDelegate, and forward each scene callback to it; that is the documented fallback for both UIKit and add-to-app hosts.
Step 3: relocate UI lifecycle logic
Most AppDelegate UI events have a one-to-one scene replacement:
Old UIApplicationDelegate method | Scene delegate equivalent |
|---|---|
applicationDidBecomeActive | sceneDidBecomeActive |
applicationWillResignActive | sceneWillResignActive |
applicationWillEnterForeground | sceneWillEnterForeground |
applicationDidEnterBackground | sceneDidEnterBackground |
application:continueUserActivity:restorationHandler: | scene:continueUserActivity: |
application:performActionForShortcutItem:completionHandler: | windowScene:performActionForShortcutItem:completionHandler: |
application:openURL:options: | scene:openURLContexts: |
application:didFinishLaunchingWithOptions: | scene:willConnectToSession:options: |
Move, do not copy: once UIKit stops invoking the old methods, a duplicated handler is a handler nobody calls. Deep links deserve a specific test here, because openURLContexts is the new home for universal links and plugins such as local_auth rely on the FlutterSceneDelegate forwarding it.
Step 4: audit plugins and platform code
Plugins that register lifecycle callbacks must adopt the scene API or they silently stop receiving events. The Flutter SDK floor for the new APIs is flutter: ">=3.38.0" with sdk: ^3.10.0, and the pattern for a migrated plugin is registering a scene delegate alongside the application delegate:
public static func register(with registrar: FlutterPluginRegistrar) {
registrar.addApplicationDelegate(instance)
registrar.addSceneDelegate(instance) // added by the migration
}
For your own native code, the deprecated-API table from the Flutter guide is the audit checklist:
| Deprecated | UIScene replacement |
|---|---|
UIScreen.mainScreen | UIWindowScene.screen |
UIApplication.keyWindow | UIWindowScene.keyWindow |
UIApplication.windows | UIWindowScene.windows |
UIApplicationDelegate.window | UIView.window |
UIApplication.shared.delegate.window is nil after adoption, so reach the view controller through the plugin registrar’s viewController property and derive window and screen from there.
One trap has a specific shape: APIs that must be configured before application:didFinishLaunchingWithOptions: completes - BGTaskScheduler.registerForTaskWithIdentifier, UNUserNotificationCenterDelegate, and HealthKit background delivery - cannot move into a scene callback. The documented pattern is for the plugin to expose a public method that your AppDelegate calls directly during launch.
The Dart side: what to re-verify
Nothing in your Dart code changes because of this migration, which is exactly why it needs a manual test pass: the code compiles, the wiring underneath it did not change, and the failures show up as behaviors that stopped happening. Work through this list on a device running an iOS 27 SDK build:
- App lifecycle observers. Exercise every
WidgetsBindingObserver.didChangeAppLifecycleStatehandler: background the app, foreground it, and confirm timers pause, streams reconnect, and analytics sessions close as before. - Deep links and inbound URLs. Cold-start universal links, warm links while the app runs, and any OAuth redirect flow, since
scene:openURLContexts:replacedapplication:openURL:options:. - Shortcuts, notifications, background work. Home screen quick actions (
windowScene:performActionForShortcutItem), notification callbacks, and scheduled background tasks - the category most likely to have been registered at the wrong point in the new sequence. - Plugin-owned lifecycle behavior. Local authentication returning to the foreground, camera permission prompts, audio sessions that react to resign-active.
- Launch-time assumptions. Anything that read
window,rootViewController, or launch options during startup.
Add-to-app and SwiftUI hosts
If Flutter is embedded in an existing iOS app, the FlutterSceneDelegate is recommended rather than required, but the value it provides is concrete: it forwards scene callbacks such as openURL to plugins. In a UIKit host, change the existing SceneDelegate to subclass FlutterSceneDelegate. In a SwiftUI host, override application(_:configurationForConnecting:options:) on the FlutterAppDelegate and return a UISceneConfiguration whose delegateClass is FlutterSceneDelegate.self.
SwiftUI hosts enable multiple scenes by default. Flutter’s guide is candid about where that leads: “Flutter doesn’t fully support multiple scenes yet, but support is in progress.” Unless you are building for iPad multi-window, set Enable Multiple Scenes to NO under the Application Scene Manifest.
Escape hatches, and their expiry date
Two opt-outs exist, both temporary:
# pubspec.yaml - hides the CLI migration warning
flutter:
config:
enable-uiscene-migration: false
and prefixing Application Scene Manifest with an underscore in Info.plist to disable UIScene support entirely. Neither moves Apple’s deadline. A build without UIScene fails to launch under Xcode 27, so treat both as a bisecting tool for your own rollout, not a release strategy.
Checklist
- Built with a current Flutter and confirmed whether the CLI auto-migrated (
Finished migration to UIScene lifecycle). - Plugin registration, method channels, and platform views moved into
didInitializeImplicitFlutterEngine, with no reads ofwindow,rootViewController, or launch options left in launch-time code. UIApplicationSceneManifestpresent inInfo.plistwithUISceneDelegateClassNamepointing atFlutterSceneDelegateor your subclass.- UI lifecycle handlers relocated from
AppDelegateto scene callbacks, with the old copies deleted rather than duplicated. - Plugins audited for
FlutterSceneLifeCycleDelegatesupport, and deprecated window/screen APIs replaced with scene-based equivalents via the registrar. - Deep links, shortcuts, notifications, background tasks, and lifecycle observers re-tested on an iOS 27 SDK build.
- Multiple scenes disabled unless you are deliberately supporting them.
- Escape hatches documented with an owner and a removal date.
Sources: UIScene adoption - Flutter breaking changes, What’s new in Flutter 3.44, Managing your app’s life cycle - Apple Developer Documentation, TN3187: Migrating to the UIKit scene-based life cycle - Apple Developer Documentation