Flutter UIScene migration for the iOS 27 SDK

Flutter UIScene migration for iOS 27: apps built with the new SDK fail to launch without it. Declaring FlutterSceneDelegate and moving channel setup fixed mine.

By

If your Flutter app stopped launching after you moved to Xcode 27, the UIScene lifecycle is the likely reason. Apple now requires it for apps built with the iOS 27 SDK, and an app that has not adopted it fails to launch on iOS 27. This post is the Flutter UIScene migration I did on a Flutter app I ship: what broke, the two changes that fixed it, and what else in your iOS code to check while you are there.

Why a Flutter app stops launching with the iOS 27 SDK

Apple announced at WWDC25 that, in the release after iOS 26, any UIKit app built with the latest SDK must use the UIScene lifecycle or it will not launch. Flutter apps are UIKit apps underneath, so they are included. Flutter's own UIScene adoption guide states it plainly: Flutter apps built with Xcode 27 that do not adopt UIScene fail to launch on startup.

On my app, Flutter refused to run it on the iOS 27 simulator at all, so there was no testing on the new OS until the migration was done.

The scene lifecycle splits responsibilities that used to live in one place. The app delegate still handles process-level events. The scene delegate now owns the UI: the window, foreground and background transitions, and opened URLs. Code that assumed a window exists during application(_:didFinishLaunchingWithOptions:) is the code that breaks.

Check whether Flutter already migrated your app

Before editing anything, check whether you need to. According to the Flutter docs, as of Flutter 3.41 UIScene support is the default, and if your AppDelegate is unmodified, running flutter run or flutter build ios migrates the project automatically. When it works, the CLI prints Finished migration to UIScene lifecycle. If it cannot migrate, it warns you and points to the manual steps.

The auto-migration only covers an untouched AppDelegate. Mine was customised, because the app has its own audio and Live Activity method channels set up there, so I did the migration by hand. Most production apps with any native code will be in the same position.

Declare FlutterSceneDelegate in Info.plist

The first change tells iOS that the app uses scenes and which class is the scene delegate. That is an Application Scene Manifest entry in ios/Runner/Info.plist, with FlutterSceneDelegate as the delegate class:

<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 forwards scene events to the Flutter engine and to plugins, so you do not need your own scene delegate unless you handle scene events yourself. If you do, the docs show how to subclass FlutterSceneDelegate and point UISceneDelegateClassName at your class instead. Multiple scenes stay off: Flutter does not fully support them yet.

Move plugin and channel setup to didInitializeImplicitFlutterEngine

The second change is where custom apps break. In the old lifecycle, plugins were registered and method channels created in application(_:didFinishLaunchingWithOptions:), usually by reaching for window?.rootViewController as a FlutterViewController. Under scenes, the window does not exist yet at that point. The Flutter docs warn that accessing the view controller there can crash the app.

Flutter's answer is a new callback. The AppDelegate conforms to FlutterImplicitEngineDelegate, and registration moves into didInitializeImplicitFlutterEngine, which receives an engine bridge with a plugin registry and a messenger. In my app, the generated plugin registration and the app's own audio and Live Activity channels all moved there. The shape follows the Flutter template and docs (channel names here are examples):

import Flutter
import UIKit

@main
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }

  func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
    GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)

    let messenger = engineBridge.applicationRegistrar.messenger()
    let audio = FlutterMethodChannel(name: "app/audio", binaryMessenger: messenger)
    audio.setMethodCallHandler { call, result in
      result(FlutterMethodNotImplemented)
    }
    let liveActivity = FlutterMethodChannel(name: "app/live_activity", binaryMessenger: messenger)
    liveActivity.setMethodCallHandler { call, result in
      result(FlutterMethodNotImplemented)
    }
  }
}

The handlers in a real app call into your audio and ActivityKit code; the stubs above just keep the example short. The important part is where the channels are created: from the bridge's messenger, inside the callback, with no reference to a window or view controller.

Audit the rest of your iOS code for scene changes

Those two changes were the fix for my app. Before calling a migration like this done, I'd go through the rest of the native code, because the scene lifecycle changes more than launch. The Flutter guide lists these, and they are the ones I would check first:

  • App-level UI lifecycle events stop arriving. After the migration, UIKit no longer calls applicationDidBecomeActive and its siblings. If your native code pauses audio or refreshes state there, it needs a scene delegate and the scene equivalents, such as sceneDidBecomeActive.
  • Launch options are nil. didFinishLaunchingWithOptions still runs, but anything that read the launch options, like a URL or a notification the app was opened from, has to move to scene(_:willConnectTo:options:).
  • The app delegate's window is nil. Code that used UIApplication.shared.delegate?.window, keyWindow or UIScreen.main should get the window from the view controller's view instead.
  • Some setup still has to happen before launch finishes. The docs call out BGTaskScheduler registration, the notification center delegate and HealthKit background delivery as APIs that must be configured before launch completes, so those stay in the app delegate.
  • Plugins need updates too. Plugins that use lifecycle events adopt FlutterSceneLifeCycleDelegate and register with addSceneDelegate. Update your plugins to versions that support scenes, and check any local plugins you maintain yourself.

Apple's technote on migrating to the scene-based life cycle covers the native side in more depth.

How I'd test a Flutter UIScene migration

Launching once is the minimum. The paths most likely to break are the ones that touch the lifecycle, so my checklist is: cold start on the iOS 27 simulator and an older iOS version you still support, background and foreground the app, open it from a deep link and from a notification, and exercise every native channel. For this app that means audio playback and starting a Live Activity.

These flows are repetitive to click through by hand on every OS version. An agent driving a debug build can do much of it, which is the setup I describe in letting an AI agent test your Flutter app. Build-level checks belong in the list too; I verify release builds with an artifact check like the canary in keeping test API keys out of production builds.

Can you delay the UIScene migration?

Only while you keep building with an older SDK. The Flutter docs describe a pubspec setting that hides the migration warning and a way to disable scene support temporarily by renaming the manifest key, but apps built with Xcode 27 that have not adopted UIScene crash on startup. Since App Store submissions move to the newest SDK over time, I'd treat the migration as due now.

The short version

  • Run flutter build ios on Flutter 3.41 or later and see if it migrates your app automatically.
  • If your AppDelegate is customised, declare FlutterSceneDelegate in Info.plist and move plugin and channel setup into didInitializeImplicitFlutterEngine.
  • Then audit lifecycle events, launch options and window access, and test on iOS 27 before your users do.

For more of the mobile work I've shipped, see my projects, and if you are adding AI features to a Flutter app next, start with adding AI features to Flutter apps without leaking API keys.

Want this kind of work on your product?

Get in touch