Flutter apps that have not adopted UIScene stop at launch when built with Xcode 27 (iOS 27 SDK), as Flutter's UIScene adoption page says. Flutter 3.41 and later migrate many apps by themselves; sdkbump tells you whether yours is one of them, and what to change when it is not.
flutter build ios migrate it?Flutter's migrator (uiscene_migration.dart, the same in 3.41.5, 3.44.9 and 3.47.5) rewrites the app only when all of these hold:
ios/Runner/Info.plist has no UIApplicationSceneManifest yet;UIMainStoryboardFile in that Info.plist is Main;ios/Runner/AppDelegate.swift is, apart from surrounding whitespace, one of the five
templates the migrator lists (or AppDelegate.h and AppDelegate.m are the Objective-C
template);enable-uiscene-migration: false in pubspec.yaml.sdkbump checks the same conditions. It compares the SHA-256 of your trimmed AppDelegate with the hashes of those templates, kept in rules/ios27.json, and when one condition fails it names it in the SCN001 message. Then:
flutter build ios. It prints
"Finished migration to UIScene lifecycle".Register plugins, method channels and platform views when the implicit engine starts, not in
application(_:didFinishLaunchingWithOptions:):
@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 battery = FlutterMethodChannel(
name: "samples.flutter.dev/battery",
binaryMessenger: engineBridge.applicationRegistrar.messenger())
battery.setMethodCallHandler { call, result in /* … */ }
}
}
Add the scene manifest to ios/Runner/Info.plist, with FlutterSceneDelegate and the Main
storyboard:
<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>
Both steps are from Flutter's UIScene adoption page, which also covers add-to-app and apps with several scenes.
window?.rootViewController as! FlutterViewControllerThis line, from the platform channel example many apps copied, is a common reason a customised
AppDelegate is not migrated automatically. Under scenes, application(_:didFinishLaunchingWithOptions:) runs before any scene
connects and UIKit does not set the app delegate's window, so the forced cast finds nothing.
Create the channel in didInitializeImplicitFlutterEngine(_:) with
engineBridge.applicationRegistrar.messenger(), as in step 1.
Overrides such as application(_:open:options:) in your FlutterAppDelegate subclass are app
delegate methods UIKit stops calling. Subclass FlutterSceneDelegate, name your subclass in
UISceneDelegateClassName, override the scene method (scene(_:openURLContexts:) for links) and
call super, so plugins still receive the event.
Plugins that rely on app delegate callbacks need the plugin API changes described on the same
Flutter page. sdkbump 0.1 checks the app's own ios/ folder; checking each plugin's iOS sources
and naming the plugin a finding comes from is planned for 0.2.