Skip to content
Blog

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’s application:didFinishLaunchingWithOptions: returns. Anything you assumed ran “at launch” may now run later.
  • window is nil earlier. Reading window?.rootViewController to grab the FlutterViewController inside didFinishLaunchingWithOptions: can crash; Flutter’s guide flags this pattern explicitly as BAD.
  • Launch options are nil. application:willFinishLaunchingWithOptions: and application:didFinishLaunchingWithOptions: are not deprecated, but their launch options come through empty after migration. Logic that depended on them belongs in scene: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:

  1. Does our AppDelegate do more than call super and register plugins?
  2. Do we embed Flutter in an existing native app (add-to-app), or ship a SwiftUI host?
  3. 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 methodScene delegate equivalent
applicationDidBecomeActivesceneDidBecomeActive
applicationWillResignActivesceneWillResignActive
applicationWillEnterForegroundsceneWillEnterForeground
applicationDidEnterBackgroundsceneDidEnterBackground
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:

DeprecatedUIScene replacement
UIScreen.mainScreenUIWindowScene.screen
UIApplication.keyWindowUIWindowScene.keyWindow
UIApplication.windowsUIWindowScene.windows
UIApplicationDelegate.windowUIView.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.didChangeAppLifecycleState handler: 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: replaced application: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 of window, rootViewController, or launch options left in launch-time code.
  • UIApplicationSceneManifest present in Info.plist with UISceneDelegateClassName pointing at FlutterSceneDelegate or your subclass.
  • UI lifecycle handlers relocated from AppDelegate to scene callbacks, with the old copies deleted rather than duplicated.
  • Plugins audited for FlutterSceneLifeCycleDelegate support, 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