Flutter iOS UISceneDelegate 迁移指南:理清新的 Scene 生命周期

引言

如果你最近升级了 Flutter 项目或 Xcode,可能已经遇到了与 UISceneDelegate 相关的警告或构建问题。

Apple 在 iOS 13 中引入了基于 Scene 的生命周期。较新的 Xcode 版本也在推动开发者使用 UISceneDelegate,而不是继续在 AppDelegate 中直接管理应用窗口。

对 Flutter 开发者来说,这次迁移可能有点让人摸不着头脑,因为老项目通常把所有初始化逻辑都放在这个文件里:

复制代码
ios/Runner/AppDelegate.swift

而采用新生命周期的 iOS 应用,需要调整这套结构。

这篇文章会介绍:

  • 为什么需要 UISceneDelegate
  • Flutter iOS 项目的结构有哪些变化
  • 如何完成迁移
  • 如何让 FlutterEngine 继续正常工作
  • 常见问题及处理方法

UISceneDelegate 是什么?

在 iOS 13 之前,iOS 应用的生命周期主要由一个对象管理:

objectivec 复制代码
UIApplicationDelegate

应用窗口也在这个文件中创建:

复制代码
AppDelegate.swift

例如:

swift 复制代码
window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = ViewController()
window?.makeKeyAndVisible()

到了 iOS 13,Apple 引入了 多个 Scene 的支持。

一个 Scene 可以理解为应用界面的一个独立实例,比如:

  • iPad 上的多个窗口
  • 外接显示器上的界面
  • 不同的应用会话

与这些界面实例相关的生命周期管理,也就从:

swift 复制代码
UIApplicationDelegate

转移到了:

swift 复制代码
UISceneDelegate

为什么 Flutter 开发者会遇到这个问题?

老的 Flutter 项目通常包含以下文件:

css 复制代码
ios/
 └── Runner/
      ├── AppDelegate.swift.swift
      ├── Info.plist
      └── Main.storyboard

AppDelegate 一般是这样写的:

swift 复制代码
import UIKit
import Flutter

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

这种写法能正常工作,是因为 Flutter 会自动创建根 FlutterViewController。

但使用 UISceneDelegate 后,窗口的生命周期管理方式就变了。

引入 UISceneDelegate 后,Flutter iOS 项目怎么组织?

推荐的结构如下:

markdown 复制代码
ios/
 └── Runner/Runner/
      ├── AppDelegate.swift
      ├── SceneDelegate.swift
      ├── Info.plist
      └── Runner.xcodeproj

两者的职责也需要分开。

AppDelegate

主要负责:

diff 复制代码
- Firebase initialization (Firebase 初始化)
- Plugin registration (插件注册)
- Background services  (后台服务)     
- Application-level events (应用级别的事件)

例如:

swift 复制代码
import UIKit
import Flutter

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

SceneDelegate

主要负责:

  • 创建 UIWindow
  • 将 FlutterViewController 设置到窗口上
  • 管理 Scene 的生命周期

例如:

swift 复制代码
import UIKit
import Flutter

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?
    func scene(
        _ scene: UIScene,
        willConnectTo session: UISceneSession,
        options connectionOptions:
        UIScene.ConnectionOptions
    ) {
        guard let windowScene =
                scene as? UIWindowScene else {
            return
        }
        let flutterViewController =
            FlutterViewController(
                engine: nil,
                nibName: nil,
                bundle: nil
            )
        let window =
            UIWindow(windowScene: windowScene)
        window.rootViewController =
            flutterViewController
        self.window = window
        window.makeKeyAndVisible()
    }
}

FlutterEngine:迁移时别忽略这一点

一个常见的坑是:直接在 SceneDelegate 中新建 FlutterViewController,却没有考虑 FlutterEngine 的管理和复用。

如果你的应用用到了以下功能,就应该复用已有的 FlutterEngine:

  • CallKit
  • Background services (后台服务)
  • MethodChannels (方法通道)
  • Push notifications (推送通知)

这里可以复用 FlutterEngine,例如:

swift 复制代码
let appDelegate = UIApplication.shared.delegate as! AppDelegate

let flutterViewController = appDelegate.window?.rootViewController as? FlutterViewController

然后将它设置为window的 rootViewController:

swift 复制代码
window.rootViewController = flutterViewController

更新 Info.plist

你还需要在 Info.plist 中注册 Scene 配置,加入以下内容:

xml 复制代码
<key>UIApplicationSceneManifest</key>
<dict>
    <key>UIApplicationSupportsMultipleScenes</key>
    <false/>
    <key>UISceneConfigurations</key>
    <dict>
        <key>UIWindowSceneSessionRoleApplication</key>
        <array>
            <dict>
                <key>UISceneConfigurationName</key>
                <string>Default Configuration</string>
                <key>UISceneDelegateClassName</key>
                <string>
                $(PRODUCT_MODULE_NAME).SceneDelegate
                </string>
            </dict>
        </array>
    </dict>
</dict>

迁移时容易踩的坑

1. UIWindow 相关逻辑仍然留在 AppDelegate 中

旧写法:

swift 复制代码
self.window = UIWindow()

迁移后,应把创建 UIWindow 的相关逻辑移到:

复制代码
SceneDelegate.swift

2. 创建了多个 FlutterEngine

错误示例:

swift 复制代码
FlutterEngine()
FlutterEngine()
FlutterEngine()

这可能导致以下功能出问题:

  • plugins (插件)
  • navigation (页面导航)
  • MethodChannels (方法通道)

所以尽量复用同一个 FlutterEngine。

3. 漏掉 GeneratedPluginRegistrant

如果少了这段注册代码:

swift 复制代码
GeneratedPluginRegistrant.register(
  with:self
)

相关插件 plugins 可能无法正常工作,比如:

  • Firebase
  • Camera
  • Notifications

4. 忘记配置 Info.plist

即使已经创建了 SceneDelegate,如果没有配置 Info.plist,iOS 也不会加载它。

5. 项目中集成了 Firebase、Agora 或 CallKit

对于集成了这些功能、业务比较复杂的 Flutter 应用,可以按下面的思路划分职责:

yaml 复制代码
Flutter App
      |
      |
AppDelegate
      |
      |
Firebase
CallKit
Push Notifications

界面UI相关的部分则放在另一侧:

markdown 复制代码
SceneDelegate
      |
      |
UIWindow
FlutterViewController

记住这个分工:

  • Services (服务相关逻辑) → AppDelegate
  • UI lifecycle (UI生命周期) → SceneDelegate

迁移后怎么验证?

迁移完成后,运行:

bash 复制代码
flutter clean

cd ios
pod deintegrate
pod install
cd ..
flutter run

然后逐项检查:

  • App launches (应用能否正常启动)
  • Navigation (页面导航是否正常)
  • Firebase 是否成功初始化(如果使用了 Firebase)
  • Push notifications (推送通知是否正常)
  • Background services (后台服务是否正常)
  • MethodChannels 是否能正常响应

最后

UISceneDelegate 迁移并不是 Flutter 独有的问题,而是为了适配 Apple 新的应用生命周期架构。

对于功能简单的 Flutter 应用,迁移工作量通常不大。

但如果是正式上线的应用,并且用到了以下功能:

  • Firebase
  • CallKit
  • Background execution (后台运行)
  • Native integrations (原生功能集成)

就需要仔细梳理职责,把这两部分分清楚:

  • AppDelegate → Application services (App服务)
  • SceneDelegate → UI lifecycle (UI生命周期)

按这个思路,完成这层职责划分后,Flutter 应用就能适配较新的 Xcode 版本,也为后续 iOS 版本的适配做好准备。

相关推荐
传奇开心果编程14 小时前
【Compose Multiplatform 跨端开发学与练】第7课 平台适配与互操作
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程19 小时前
【现代声明式UI学与练】第4课 列表渲染与 key——如何高效渲染列表、key 的作用、列表重排时的状态保持
学习·flutter·react native·ui·swiftui·android jetpack
传奇开心果编程19 小时前
【Compose Multiplatform 跨端开发学与练】第5课 网络与数据层
android·网络·学习·ui·ios·kotlin·composer
茶底世界之下20 小时前
视频预览切换为何会闪回旧帧:用 generation + mode identity 管住异步回调
ios·swift
m0_7381858221 小时前
Flutter 鸿蒙化实战:media_info 适配 OpenHarmony,媒体信息与缩略图
flutter·华为·harmonyos·鸿蒙·媒体
老李IT笔记1 天前
从备份恢复会把管理状态带回来吗:iOS 27 之后答案变了|MDM.Plus
ios·智能手机
谢亮_vipxieliang1 天前
Go 接口设计原则核心知识点
开发语言·ios·golang
m0_738185821 天前
Flutter 鸿蒙化实战:just_audio 适配 OpenHarmony,功能强大的播放器
flutter·华为·harmonyos·鸿蒙
Hello_Pyhx1 天前
iPhoneMirror:把 iPhone 接入 Windows,先分清投屏与反控
windows·ios·iphone
茶底世界之下1 天前
类型擦除之后,Metal 滤镜组合为什么不能只执行一个 Filter?
ios·swift