Flutter iOS 项目 UIScene 迁移指南


一、UIScene 是什么?

在 iOS 13 发布时,Apple 引入了 UIScene ,正式把一个 App 的生命周期从「单个进程」拆成了「多个场景(Scene)」。

简单理解:

旧时代(AppDelegate) 新时代(UIScene)
一个 App 只有一个窗口 一个 App 可以同时有多个窗口(iPad 分屏、外接屏、多任务)
所有 UI 生命周期都在 AppDelegate 每个窗口(Scene)都有自己的 SceneDelegate 管理生命周期
applicationDidBecomeActive 等方法全局生效 每个 Scene 独立触发 sceneDidBecomeActive

二、2025 年最硬的刀:iOS 26 强制适配 UIScene 警告

未进行适配的iOS项目,运行后会有一个红色的警告:

三、Flutter 项目 UIScene 迁移步骤

前置条件

yaml 复制代码
# pubspec.yaml
environment:
  sdk: ^3.10.0
  flutter: ">=3.38.0" 

方式一:自动迁移(推荐先试)

bash 复制代码
flutter config --enable-uiscene-migration
flutter clean
flutter run

看到 Finished migration to UIScene lifecycle 就说明成功了,直接跳到最后测试即可。如果有自定义代码,可能会提示手动迁移。

方式二:手动迁移(自定义过 AppDelegate 的项目)

1. 改造 AppDelegate

  • Swift
swift 复制代码
import UIKit
import Flutter

@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)
    
    // 你原来的 MethodChannel、PlatformView、原生代码全部搬到这里
    let channel = FlutterMethodChannel(
      name: "your_channel",
      binaryMessenger: engineBridge.applicationRegistrar.messenger()
    )
  }
}

2. 添加 Application Scene Manifest

打开 ios/Runner/Info.plist,加入以下内容:

plist 复制代码
<key>UIApplicationSceneManifest</key>
<dict>
    <key>UIApplicationSupportsMultipleScenes</key>
    <false/> <!-- 普通 App 保持 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>

3. (可选)创建自定义 SceneDelegate

如果你原来在 AppDelegate 里写了前后台逻辑,全部搬到这里:

swift 复制代码
// SceneDelegate.swift
import UIKit
import Flutter

class SceneDelegate: FlutterSceneDelegate {
  
  override func sceneDidBecomeActive(_ scene: UIScene) {
    super.sceneDidBecomeActive(scene)
    // 原来 applicationDidBecomeActive 的代码
  }
  
  override func sceneWillResignActive(_ scene: UIScene) {
    super.sceneWillResignActive(scene)
    // 原来 applicationWillResignActive 的代码
  }
}

然后把 Info.plist 里的 UISceneDelegateClassName 改成:

plist 复制代码
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>

4. 清理重建

bash 复制代码
flutter clean
cd ios && pod install --repo-update && cd ..
flutter run

四、Add-to-App (ATA) 项目迁移

如果你的 Flutter 是嵌入现有 iOS App(Add-to-App),迁移更复杂,因为需要手动管理 FlutterEngine 和场景生命周期。以下是详细步骤(基于 Flutter 官方指南):

1. 在 application:configurationForConnecting:options: 中设置 Delegate Class

在你的现有 AppDelegate 中,为连接的场景配置 FlutterSceneDelegate。这确保每个新场景使用 Flutter 的场景委托。

Swift 示例:

swift 复制代码
override func application(
  _ application: UIApplication,
  configurationForConnecting connectingSceneSession: UISceneSession,
  options: UIScene.ConnectionOptions
) -> UISceneConfiguration {
  let configuration = UISceneConfiguration(
    name: nil,
    sessionRole: connectingSceneSession.role
  )
  configuration.delegateClass = FlutterSceneDelegate.self  // 设置为 FlutterSceneDelegate
  return configuration
}

Objective-C 示例:

OC 复制代码
- (UISceneConfiguration *)application:(UIApplication *)application
    configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession
    options:(UISceneConnectionOptions *)options {
        UISceneConfiguration *configuration = [[UISceneConfiguration alloc] initWithName:nil sessionRole:connectingSceneSession.role];
        configuration.delegateClass = [FlutterSceneDelegate class];
        return configuration;
    }

2. 禁用多场景支持(除非必要)

Info.plist 中设置 UIApplicationSupportsMultipleScenesNO,避免不必要的多窗口复杂性。除非你的 App 明确需要 iPad 分屏或外部显示器支持。

plist 复制代码
<key>UIApplicationSupportsMultipleScenes</key>
<false/>

3. 手动创建并运行 FlutterEngine

在 SceneDelegate 的 scene:willConnectToSession:options: 中手动创建 FlutterEngine,运行它,注册插件,并设置视图层次。Flutter 不会自动处理引擎创建。

Swift 示例(子类 FlutterSceneDelegate):

swift 复制代码
class SceneDelegate: FlutterSceneDelegate {
  let flutterEngine = FlutterEngine(name: "my flutter engine")  // 手动创建引擎

  override func scene(
    _ scene: UIScene,
    willConnectTo session: UISceneSession,
    options connectionOptions: UIScene.ConnectionOptions
  ) {
    guard let windowScene = scene as? UIWindowScene else { return }
    window = UIWindow(windowScene: windowScene)

    flutterEngine.run()  // 运行引擎
    GeneratedPluginRegistrant.register(with: flutterEngine)  // 注册插件
    self.registerSceneLifeCycle(with: flutterEngine)  // 注册场景生命周期

    let viewController = ViewController(engine: flutterEngine)  // 创建 FlutterViewController
    window?.rootViewController = viewController
    window?.makeKeyAndVisible()
    
    super.scene(scene, willConnectTo: session, options: connectionOptions)
  }
}

Objective-C 示例:

OC 复制代码
@interface SceneDelegate : FlutterSceneDelegate
@property (nonatomic, strong) FlutterEngine *flutterEngine;
@end

@implementation SceneDelegate

- (instancetype)init {
    if (self = [super init]) {
        _flutterEngine = [[FlutterEngine alloc] initWithName:@"my flutter engine"];
    }
    return self;
}

- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session
                                    options:(UISceneConnectionOptions *)connectionOptions {
                                        if (![scene isKindOfClass:[UIWindowScene class]]) {
                                            return;
                                        }
                                        UIWindowScene *windowScene = (UIWindowScene *)scene;
                                        self.window = [[UIWindow alloc] initWithWindowScene:windowScene];

                                        [self.flutterEngine run];
                                        [GeneratedPluginRegistrant registerWithRegistry:self.flutterEngine];
                                        [self registerSceneLifeCycleWithFlutterEngine:self.flutterEngine];

                                        ViewController *viewController = [[ViewController alloc] initWithEngine:self.flutterEngine];
                                        self.window.rootViewController = viewController;
                                        [self.window makeKeyAndVisible];

                                        [super scene:scene willConnectToSession:session options:connectionOptions];
                                    }
@end

4. 注册和注销场景生命周期

  • 注册(registerSceneLifeCycle) :在 willConnectToSession 中调用,确保引擎接收场景回调。
  • 注销(unregisterSceneLifeCycle) :在场景断开(如 sceneDidDisconnect)或切换时调用,避免内存泄漏。

Swift 示例:

swift 复制代码
// 注册
self.registerSceneLifeCycle(with: flutterEngine)

// 注销(例如在 sceneDidDisconnect 中)
self.unregisterSceneLifeCycle(with: flutterEngine)

Objective-C 示例:

OC 复制代码
// 注册
[self registerSceneLifeCycleWithFlutterEngine:self.flutterEngine];

// 注销
[self unregisterSceneLifeCycleWithFlutterEngine:self.flutterEngine];

5. 替代方案:实现 FlutterSceneLifeCycleProvider

如果无法直接子类 FlutterSceneDelegate,实现 FlutterSceneLifeCycleProvider 协议,手动转发回调。

Swift 示例:

swift 复制代码
class SceneDelegate: UIResponder, UIWindowSceneDelegate, FlutterSceneLifeCycleProvider {
  var sceneLifeCycleDelegate: FlutterPluginSceneLifeCycleDelegate =
    FlutterPluginSceneLifeCycleDelegate()

  func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options: UIScene.ConnectionOptions) {
    sceneLifeCycleDelegate.scene(scene, willConnectTo: session, options: options)
    // 其他设置...
  }

  // 实现其他场景回调,如 sceneDidBecomeActive 等
}

6. 多场景支持

如果启用多场景,为每个场景单独创建 FlutterEngine,并在连接/断开时注册/注销。忘记注销可能导致重复回调或泄漏。

参考 Apple 指南:Migrating to the UIKit Scene-Based Life Cycle

五、Flutter 插件迁移

如果你的插件依赖 UI 生命周期(如权限请求、前后台切换),必须适配场景委托。并非所有插件都需要改;仅那些使用 UIApplicationDelegate UI 事件的才需更新。以下是详细步骤:

1. 更新 pubspec.yaml

确保插件支持新版本。

yaml 复制代码
environment:
  sdk: ^3.10.0
  flutter: ">=3.38.0"

2. 采用 FlutterSceneLifeCycleDelegate

更新插件主类,实现协议以接收场景回调。

Swift 示例:

swift 复制代码
public final class MyPlugin: NSObject, FlutterPlugin, FlutterSceneLifeCycleDelegate {
  // 插件逻辑...
}

Objective-C 示例:

OC 复制代码
@interface MyPlugin : NSObject <FlutterPlugin, FlutterSceneLifeCycleDelegate>
// 插件逻辑...
@end

3. 注册插件为场景委托接收者

register(with:) 中,同时注册为 AppDelegate 和 SceneDelegate,支持向后兼容。

Swift 示例:

swift 复制代码
public static func register(with registrar: FlutterPluginRegistrar) {
  let instance = MyPlugin()
  registrar.addApplicationDelegate(instance)  // 保持 AppDelegate 支持
  registrar.addSceneDelegate(instance)  // 添加 SceneDelegate 支持
  // 其他注册,如 MethodChannel...
}

Objective-C 示例:

OC 复制代码
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
    MyPlugin *instance = [[MyPlugin alloc] init];
    [registrar addApplicationDelegate:instance];
    [registrar addSceneDelegate:instance];
    // 其他注册...
}

4. 实现场景生命周期回调

实现协议方法,替换旧的 AppDelegate UI 事件。每个方法对应一个场景事件。

Swift 示例(关键方法):

swift 复制代码
// 场景连接(处理启动选项)
public func scene(
  _ scene: UIScene,
  willConnectTo session: UISceneSession,
  options connectionOptions: UIScene.ConnectionOptions?
) -> Bool {
  // 从 connectionOptions 处理启动 URL、UserActivity 等
  // 原来在 didFinishLaunchingWithOptions 的逻辑搬到这里
  return true
}

// 其他回调
public func sceneDidDisconnect(_ scene: UIScene) { /* 清理资源 */ }
public func sceneWillEnterForeground(_ scene: UIScene) { /* 前台准备 */ }
public func sceneDidBecomeActive(_ scene: UIScene) { /* 激活逻辑 */ }
public func sceneWillResignActive(_ scene: UIScene) { /* 暂停逻辑 */ }
public func sceneDidEnterBackground(_ scene: UIScene) { /* 后台处理 */ }
public func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) -> Bool { /* URL 处理 */ }
public func scene(_ scene: UIScene, continue userActivity: NSUserActivity) -> Bool { /* 续接活动 */ }
public func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) -> Bool { /* 快捷方式 */ }

Objective-C 示例(关键方法):

OC 复制代码
- (BOOL)scene:(UIScene*)scene willConnectToSession:(UISceneSession*)session options:(nullable UISceneConnectionOptions*)connectionOptions {
    // 处理启动选项
    return YES;
}

// 其他回调
- (void)sceneDidDisconnect:(UIScene*)scene { }
- (void)sceneWillEnterForeground:(UIScene*)scene { }
- (void)sceneDidBecomeActive:(UIScene*)scene { }
- (void)sceneWillResignActive:(UIScene*)scene { }
- (void)sceneDidEnterBackground:(UIScene*)scene { }
- (BOOL)scene:(UIScene*)scene openURLContexts:(NSSet<UIOpenURLContext*>*)URLContexts { return YES; }
- (BOOL)scene:(UIScene*)scene continueUserActivity:(NSUserActivity*)userActivity { return YES; }
- (BOOL)windowScene:(UIWindowScene*)windowScene performActionForShortcutItem:(UIApplicationShortcutItem*)shortcutItem completionHandler:(void (^)(BOOL))completionHandler { return YES; }

5. 迁移启动逻辑

将从 application:didFinishLaunchingWithOptions: 的逻辑移到 scene:willConnectToSession:options:,因为 UIScene 后 launchOptions 可能为 nil。

6. 测试与发布

  • 测试所有 UI 事件(如推送、权限)。
  • 保持 AppDelegate 注册以支持旧 App。

六、最后

做完以上,你的 Flutter iOS 项目就彻底拥抱了 UIScene 了。

官方文档:
docs.flutter.dev/release/bre...

相关推荐
wordbaby27 分钟前
赋值即响应:深入剖析 Riverpod 的“核心引擎”
前端·flutter
iFlow_AI1 小时前
iFlow CLI快速搭建Flutter应用记录
开发语言·前端·人工智能·flutter·ai·iflow·iflow cli
Antonio9152 小时前
【Swift】 UIKit:UIGestureRecognizer和UIView Animation
开发语言·ios·swift
恋猫de小郭3 小时前
回顾 Flutter Flight Plans ,关于 Flutter 的现状和官方热门问题解答
android·前端·flutter
张风捷特烈3 小时前
FlutterUnit3.4.1 | 来场三方库的收录狂欢吧~
android·前端·flutter
私人珍藏库5 小时前
利用 iPhone 或 Apple Watch ,自动锁定和解锁 Windows
ios·iphone
2501_9160088911 小时前
iOS 性能测试的深度实战方法 构建从底层指标到真实场景回放的多工具测试体系
android·ios·小程序·https·uni-app·iphone·webview
kk哥889911 小时前
iOS 26 适配指南:UIScrollView 新特性与最佳实践
macos·ios·cocoa
iOS阿玮12 小时前
屁股坏了,我硬抗了3天,差不多省了几千块这件事儿。
ios