引言
如果你最近升级了 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 版本的适配做好准备。