skip to content

Why would a Flutter iOS app built with the Xcode 27 SDK crash at launch, and what does the UIScene migration change in its iOS project?

level: seniorimportance: should knowfreq 22%

answer

  1. Apple mandates the scene lifecycle
  2. landed 3.38, default 3.41
  3. unmodified AppDelegate migrates automatically
  4. didInitializeImplicitFlutterEngine registers plugins
  5. UIApplicationSceneManifest in Info.plist

basics

~20 s

Apple requires the UIScene lifecycle for UIKit apps built with the iOS 27 SDK, and a Flutter app that has not adopted it crashes at launch; migration adds a scene manifest to Info.plist and moves plugin registration out of didFinishLaunching.

solid answer

~30 s

Starting with Xcode 27 (the iOS 27 SDK), UIKit apps must use the **UIScene lifecycle**, and a Flutter app that has not adopted it crashes on startup. Flutter added support in 3.38 and made it the default in 3.41, when `flutter run` or `flutter build ios` auto-migrates a project whose `AppDelegate` is unmodified and prints `Finished migration to UIScene lifecycle`. A customised `AppDelegate` must be migrated by hand: conform to `FlutterImplicitEngineDelegate`, move `GeneratedPluginRegistrant.register` and any channel setup into `didInitializeImplicitFlutterEngine`, move UI-state lifecycle logic into a `SceneDelegate` subclassing `FlutterSceneDelegate`, and add `UIApplicationSceneManifest` to `Info.plist`. Plugins that use app lifecycle events adopt `FlutterSceneLifeCycleDelegate`.

code

swift · 16 lines
swift
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)
  }
}

go deeper

for a junior

Recall that newer Xcode versions require the UIScene lifecycle and that Flutter can migrate an untouched AppDelegate automatically.

for a middle

Explain what moves: plugin registration into didInitializeImplicitFlutterEngine, UI callbacks into a FlutterSceneDelegate, and the scene manifest into Info.plist.

for a senior

Diagnose a launch crash after an Xcode upgrade, migrate a customised AppDelegate safely, and audit plugins that depend on app-delegate lifecycle events.

for a principal

Plan toolchain upgrades as release risks: pin Xcode on build machines and budget platform migrations before Apple's deadline.

## What changed on Apple's side UIKit has two lifecycle models. The older one routes everything through the **app delegate**: launch, becoming active, going to the background. The **scene** model, introduced for multi-window iPad apps, splits the UI lifecycle out into a **scene delegate**, while the app delegate keeps process-level events. Apple announced that, in the release after iOS 26, any UIKit app built with the latest SDK must use the scene lifecycle or it will not launch. For Flutter this is concrete: **apps built with Xcode 27 (iOS 27 SDK) that have not adopted `UIScene` crash on startup.** The code did not change; a toolchain upgrade breaks it, which is why this surfaces as a release-day failure. ## Flutter's timeline | Release | What it brought | |---|---| | 3.38 | `UIScene` support and the APIs plugins need | | 3.41 | `UIScene` on by default, new templates, automatic migration of unmodified projects | | 3.47 (pinned) | templates ship `AppDelegate` with `FlutterImplicitEngineDelegate` and a `SceneDelegate` | ## The automatic path If the project's `AppDelegate` is still the template's, running `flutter run` or `flutter build ios` on 3.41+ migrates it and prints `Finished migration to UIScene lifecycle`. If the file was customised, the tool warns and links to the manual steps. The warning can be silenced with `enable-uiscene-migration: false` under `flutter: config:` in `pubspec.yaml`, which hides the reminder without fixing anything. ## The manual migration 1. **AppDelegate.** Conform to `FlutterImplicitEngineDelegate` and move `GeneratedPluginRegistrant.register(with:)` into `didInitializeImplicitFlutterEngine(_ engineBridge:)`, registering with `engineBridge.pluginRegistry`. 2. **Channels and platform views.** Anything created in `application(_:didFinishLaunchingWithOptions:)` moves to the same callback, using `engineBridge.applicationRegistrar.messenger()`. Reaching for `window?.rootViewController as! FlutterViewController` during launch can crash, because the view controller now belongs to the scene. 3. **UI lifecycle callbacks.** Methods such as `applicationDidBecomeActive` are no longer called once scenes are adopted. Move that logic into a `SceneDelegate` that subclasses `FlutterSceneDelegate`, or conforms to `FlutterSceneLifeCycleProvider` when subclassing is impossible. 4. **Info.plist.** Add `UIApplicationSceneManifest` with `UIApplicationSupportsMultipleScenes` set to false and a `UIWindowSceneSessionRoleApplication` configuration naming `UIWindowScene`, the scene delegate class, the configuration name `flutter` and the `Main` storyboard. ## Plugins A plugin that listens to app lifecycle events adopts `FlutterSceneLifeCycleDelegate` and registers itself with `registrar.addSceneDelegate(...)`, requiring Flutter 3.38 or later in its `pubspec.yaml`. An app whose own code migrated can still misbehave if a plugin it depends on still waits for app-delegate callbacks that no longer fire. ## Diagnosing it in practice - A crash at launch that appears only after upgrading Xcode, with no Dart change, points here first. - Check whether `Info.plist` contains `UIApplicationSceneManifest`; for a quick bisect the docs allow prefixing it with an underscore to disable scenes temporarily. - Search the `AppDelegate` for registration or channel setup still inside `didFinishLaunchingWithOptions`. - Audit plugins that react to foreground and background transitions. ## Rolling it out safely Because the failure is triggered by the toolchain rather than by code, the safest order is to migrate before the build machines move to the new Xcode: 1. Upgrade Flutter to 3.41 or later and let the tool migrate, or migrate a customised `AppDelegate` by hand. 2. Test launch, cold start from a notification or link, backgrounding and returning, since those paths are where app-delegate callbacks used to run. 3. Upgrade plugins that list scene support in their changelogs. 4. Only then move CI and developer Macs to Xcode 27. Add-to-app projects, where Flutter is embedded in an existing native iOS app, follow a separate section of the migration guide; the native host owns the scene delegate there and uses `FlutterSceneDelegate` or `FlutterSceneLifeCycleProvider` to forward scene events. ## Common misconceptions - Adopting scenes does not make a Flutter app multi-window; the template sets multiple scenes to false and the docs say Flutter does not fully support them yet. - Hiding the warning is not a migration. - `AppLifecycleState` in Dart keeps working; the change is in the native layer that feeds it.

  • Does setting enable-uiscene-migration: false in pubspec.yaml keep an unmigrated app launching under Xcode 27?
    No. That key only hides the Flutter CLI's migration warning. The crash comes from UIKit requiring the scene lifecycle for apps built with the iOS 27 SDK, so the project still has to be migrated.
  • After migrating, a custom method channel set up in didFinishLaunchingWithOptions stops responding; why?
    With scenes the Flutter view controller is created by the scene, so launch-time code cannot rely on it. Create the channel in `didInitializeImplicitFlutterEngine` with `engineBridge.applicationRegistrar.messenger()`.

saying these in an interview costs you the question

  • The crash must be a Dart bug because it appeared with a new release build.
  • Setting enable-uiscene-migration: false fixes the launch crash.
  • Adopting UIScene makes a Flutter app support multiple windows.
  • applicationDidBecomeActive still fires in the AppDelegate after migrating.
  • Only apps with custom native code are affected; plain Flutter apps are exempt.