將 Flutter 螢幕新增到 iOS 應用
瞭解如何將單個 Flutter 螢幕新增到現有的 iOS 應用。
本指南介紹瞭如何將單個 Flutter 螢幕新增到現有 iOS 應用。
啟動 FlutterEngine 和 FlutterViewController
#要從現有 iOS 應用啟動 Flutter 螢幕,需要啟動一個 FlutterEngine 和一個 FlutterViewController。
FlutterEngine 的生命週期可能與 FlutterViewController 相同,也可能比 FlutterViewController 更長。
有關預熱引擎的延遲和記憶體權衡的更多分析,請參閱 載入序列和效能。
建立 FlutterEngine
#建立 FlutterEngine 的位置取決於你的宿主應用程式。
在這個示例中,我們在一個名為 FlutterDependencies 的 SwiftUI Observable 物件中建立了一個 FlutterEngine 物件。透過呼叫 run() 預熱引擎,然後使用 environment() 檢視修飾符將此物件注入 ContentView。
import SwiftUI
import Flutter
// The following library connects plugins with iOS platform code to this app.
import FlutterPluginRegistrant
@Observable
class FlutterDependencies {
let flutterEngine = FlutterEngine(name: "my flutter engine")
init() {
// Runs the default Dart entrypoint with a default Flutter route.
flutterEngine.run()
// Connects plugins with iOS platform code to this app.
GeneratedPluginRegistrant.register(with: self.flutterEngine);
}
}
@main
struct MyApp: App {
// flutterDependencies will be injected through the view environment.
@State var flutterDependencies = FlutterDependencies()
var body: some Scene {
WindowGroup {
ContentView()
.environment(flutterDependencies)
}
}
}
舉例來說,我們演示了在應用程式啟動時在應用程式委託中建立一個 FlutterEngine,並將其作為屬性公開。
import UIKit
import Flutter
// The following library connects plugins with iOS platform code to this app.
import FlutterPluginRegistrant
@UIApplicationMain
class AppDelegate: FlutterAppDelegate { // More on the FlutterAppDelegate.
lazy var flutterEngine = FlutterEngine(name: "my flutter engine")
override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Runs the default Dart entrypoint with a default Flutter route.
flutterEngine.run();
// Connects plugins with iOS platform code to this app.
GeneratedPluginRegistrant.register(with: self.flutterEngine);
return super.application(application, didFinishLaunchingWithOptions: launchOptions);
}
}
以下示例演示了在應用程式啟動時在應用程式委託中建立一個 FlutterEngine,並將其作為屬性公開。
@import UIKit;
@import Flutter;
@interface AppDelegate : FlutterAppDelegate // More on the FlutterAppDelegate below.
@property (nonatomic,strong) FlutterEngine *flutterEngine;
@end
// The following library connects plugins with iOS platform code to this app.
#import <FlutterPluginRegistrant/GeneratedPluginRegistrant.h>
#import "AppDelegate.h"
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary<UIApplicationLaunchOptionsKey, id> *)launchOptions {
self.flutterEngine = [[FlutterEngine alloc] initWithName:@"my flutter engine"];
// Runs the default Dart entrypoint with a default Flutter route.
[self.flutterEngine run];
// Connects plugins with iOS platform code to this app.
[GeneratedPluginRegistrant registerWithRegistry:self.flutterEngine];
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
@end
使用 FlutterEngine 顯示 FlutterViewController
#以下示例展示了一個通用的 ContentView,其中包含一個連線到 flutter 螢幕的 NavigationLink。首先,建立一個 FlutterViewControllerRepresentable 來表示 FlutterViewController。FlutterViewController 建構函式將預熱的 FlutterEngine 作為引數,並透過檢視環境注入。
import SwiftUI
import Flutter
struct FlutterViewControllerRepresentable: UIViewControllerRepresentable {
// Flutter dependencies are passed in through the view environment.
@Environment(FlutterDependencies.self) var flutterDependencies
func makeUIViewController(context: Context) -> some UIViewController {
return FlutterViewController(
engine: flutterDependencies.flutterEngine,
nibName: nil,
bundle: nil)
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {}
}
struct ContentView: View {
var body: some View {
NavigationStack {
NavigationLink("My Flutter Feature") {
FlutterViewControllerRepresentable()
}
}
}
}
現在,你的 iOS 應用中嵌入了一個 Flutter 螢幕。
以下示例顯示了一個通用的 ViewController,其中包含一個連線到顯示 FlutterViewController 的 UIButton。FlutterViewController 使用在 AppDelegate 中建立的 FlutterEngine 例項。
import UIKit
import Flutter
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Make a button to call the showFlutter function when pressed.
let button = UIButton(type:UIButton.ButtonType.custom)
button.addTarget(self, action: #selector(showFlutter), for: .touchUpInside)
button.setTitle("Show Flutter!", for: UIControl.State.normal)
button.frame = CGRect(x: 80.0, y: 210.0, width: 160.0, height: 40.0)
button.backgroundColor = UIColor.blue
self.view.addSubview(button)
}
@objc func showFlutter() {
let flutterEngine = (UIApplication.shared.delegate as! AppDelegate).flutterEngine
let flutterViewController =
FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil)
present(flutterViewController, animated: true, completion: nil)
}
}
現在,你的 iOS 應用中嵌入了一個 Flutter 螢幕。
以下示例顯示了一個通用的 ViewController,其中包含一個連線到顯示 FlutterViewController 的 UIButton。FlutterViewController 使用在 AppDelegate 中建立的 FlutterEngine 例項。
@import Flutter;
#import "AppDelegate.h"
#import "ViewController.h"
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Make a button to call the showFlutter function when pressed.
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
[button addTarget:self
action:@selector(showFlutter)
forControlEvents:UIControlEventTouchUpInside];
[button setTitle:@"Show Flutter!" forState:UIControlStateNormal];
button.backgroundColor = UIColor.blueColor;
button.frame = CGRectMake(80.0, 210.0, 160.0, 40.0);
[self.view addSubview:button];
}
- (void)showFlutter {
FlutterEngine *flutterEngine =
((AppDelegate *)UIApplication.sharedApplication.delegate).flutterEngine;
FlutterViewController *flutterViewController =
[[FlutterViewController alloc] initWithEngine:flutterEngine nibName:nil bundle:nil];
[self presentViewController:flutterViewController animated:YES completion:nil];
}
@end
現在,你的 iOS 應用中嵌入了一個 Flutter 螢幕。
或者 - 使用隱式 FlutterEngine 建立 FlutterViewController
#作為前面示例的替代方案,你可以讓 FlutterViewController 隱式建立自己的 FlutterEngine,而無需提前預熱。
這通常不建議,因為按需建立 FlutterEngine 可能會在 FlutterViewController 呈現和渲染其第一幀之間引入明顯的延遲。然而,如果 Flutter 螢幕很少顯示,沒有好的啟發式方法來確定何時啟動 Dart VM,並且 Flutter 不需要持久化檢視控制器之間的狀態時,這可能很有用。
要讓 FlutterViewController 在沒有現有 FlutterEngine 的情況下呈現,請省略 FlutterEngine 的構造,並在沒有引擎引用的情況下建立 FlutterViewController。
import SwiftUI
import Flutter
struct FlutterViewControllerRepresentable: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> some UIViewController {
return FlutterViewController(
project: nil,
nibName: nil,
bundle: nil)
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {}
}
struct ContentView: View {
var body: some View {
NavigationStack {
NavigationLink("My Flutter Feature") {
FlutterViewControllerRepresentable()
}
}
}
}
// Existing code omitted.
func showFlutter() {
let flutterViewController = FlutterViewController(project: nil, nibName: nil, bundle: nil)
present(flutterViewController, animated: true, completion: nil)
}
// Existing code omitted.
- (void)showFlutter {
FlutterViewController *flutterViewController =
[[FlutterViewController alloc] initWithProject:nil nibName:nil bundle:nil];
[self presentViewController:flutterViewController animated:YES completion:nil];
}
@end
有關延遲和記憶體使用的更多探索,請參閱 載入序列和效能。
使用 FlutterAppDelegate
#建議你的應用程式的 UIApplicationDelegate 子類化 FlutterAppDelegate,但這不是必需的。
FlutterAppDelegate 執行以下功能:
- 將應用程式回撥(例如
openURL)轉發到外掛,例如 local_auth。 - 在除錯模式下,當手機螢幕鎖定時,保持 Flutter 連線開啟。
建立 FlutterAppDelegate 子類
#在 UIKit 應用程式中建立 FlutterAppDelegate 的子類已在 啟動 FlutterEngine 和 FlutterViewController 部分中展示。在 SwiftUI 應用程式中,你可以建立 FlutterAppDelegate 的子類並使用 Observable() 宏對其進行註釋,如下所示
import SwiftUI
import Flutter
import FlutterPluginRegistrant
@Observable
class AppDelegate: FlutterAppDelegate {
let flutterEngine = FlutterEngine(name: "my flutter engine")
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Runs the default Dart entrypoint with a default Flutter route.
flutterEngine.run();
// Used to connect plugins (only if you have plugins with iOS platform code).
GeneratedPluginRegistrant.register(with: self.flutterEngine);
return true;
}
}
@main
struct MyApp: App {
// Use this property wrapper to tell SwiftUI
// it should use the AppDelegate class for the application delegate
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
然後,在你的檢視中,可以透過檢視環境訪問 AppDelegate。
import SwiftUI
import Flutter
struct FlutterViewControllerRepresentable: UIViewControllerRepresentable {
// Access the AppDelegate through the view environment.
@Environment(AppDelegate.self) var appDelegate
func makeUIViewController(context: Context) -> some UIViewController {
return FlutterViewController(
engine: appDelegate.flutterEngine,
nibName: nil,
bundle: nil)
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {}
}
struct ContentView: View {
var body: some View {
NavigationStack {
NavigationLink("My Flutter Feature") {
FlutterViewControllerRepresentable()
}
}
}
}
如果不能直接將 FlutterAppDelegate 子類化
#如果您的應用程式委託不能直接將 FlutterAppDelegate 子類化,請讓您的應用程式委託實現 FlutterAppLifeCycleProvider 協議,以確保您的外掛接收到必要的回撥。否則,依賴這些事件的外掛可能會出現未定義的行為。
例如
import Foundation
import Flutter
@Observable
class AppDelegate: UIResponder, UIApplicationDelegate, FlutterAppLifeCycleProvider {
private let lifecycleDelegate = FlutterPluginAppLifeCycleDelegate()
let flutterEngine = FlutterEngine(name: "my flutter engine")
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
flutterEngine.run()
return lifecycleDelegate.application(application, didFinishLaunchingWithOptions: launchOptions ?? [:])
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
lifecycleDelegate.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken)
}
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
lifecycleDelegate.application(application, didFailToRegisterForRemoteNotificationsWithError: error)
}
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
lifecycleDelegate.application(application, didReceiveRemoteNotification: userInfo, fetchCompletionHandler: completionHandler)
}
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
return lifecycleDelegate.application(app, open: url, options: options)
}
func application(_ application: UIApplication, handleOpen url: URL) -> Bool {
return lifecycleDelegate.application(application, handleOpen: url)
}
func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool {
return lifecycleDelegate.application(application, open: url, sourceApplication: sourceApplication ?? "", annotation: annotation)
}
func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
lifecycleDelegate.application(application, performActionFor: shortcutItem, completionHandler: completionHandler)
}
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
lifecycleDelegate.application(application, handleEventsForBackgroundURLSession: identifier, completionHandler: completionHandler)
}
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
lifecycleDelegate.application(application, performFetchWithCompletionHandler: completionHandler)
}
func add(_ delegate: FlutterApplicationLifeCycleDelegate) {
lifecycleDelegate.add(delegate)
}
}
@import Flutter;
@import UIKit;
@import FlutterPluginRegistrant;
@interface AppDelegate : UIResponder <UIApplicationDelegate, FlutterAppLifeCycleProvider>
@property (strong, nonatomic) UIWindow *window;
@property (nonatomic,strong) FlutterEngine *flutterEngine;
@end
該實現應主要委託給 FlutterPluginAppLifeCycleDelegate
@interface AppDelegate ()
@property (nonatomic, strong) FlutterPluginAppLifeCycleDelegate* lifeCycleDelegate;
@end
@implementation AppDelegate
- (instancetype)init {
if (self = [super init]) {
_lifeCycleDelegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
}
return self;
}
- (BOOL)application:(UIApplication*)application
didFinishLaunchingWithOptions:(NSDictionary<UIApplicationLaunchOptionsKey, id>*))launchOptions {
self.flutterEngine = [[FlutterEngine alloc] initWithName:@"io.flutter" project:nil];
[self.flutterEngine runWithEntrypoint:nil];
[GeneratedPluginRegistrant registerWithRegistry:self.flutterEngine];
return [_lifeCycleDelegate application:application didFinishLaunchingWithOptions:launchOptions];
}
// Returns the key window's rootViewController, if it's a FlutterViewController.
// Otherwise, returns nil.
- (FlutterViewController*)rootFlutterViewController {
UIViewController* viewController = [UIApplication sharedApplication].keyWindow.rootViewController;
if ([viewController isKindOfClass:[FlutterViewController class]]) {
return (FlutterViewController*)viewController;
}
return nil;
}
- (void)application:(UIApplication*)application
didRegisterUserNotificationSettings:(UIUserNotificationSettings*)notificationSettings {
[_lifeCycleDelegate application:application
didRegisterUserNotificationSettings:notificationSettings];
}
- (void)application:(UIApplication*)application
didRegisterForRemoteNotificationsWithDeviceToken:(NSData*)deviceToken {
[_lifeCycleDelegate application:application
didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
}
- (void)application:(UIApplication*)application
didReceiveRemoteNotification:(NSDictionary*)userInfo
fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler {
[_lifeCycleDelegate application:application
didReceiveRemoteNotification:userInfo
fetchCompletionHandler:completionHandler];
}
- (BOOL)application:(UIApplication*)application
openURL:(NSURL*)url
options:(NSDictionary<UIApplicationOpenURLOptionsKey, id>*)options {
return [_lifeCycleDelegate application:application openURL:url options:options];
}
- (BOOL)application:(UIApplication*)application handleOpenURL:(NSURL*)url {
return [_lifeCycleDelegate application:application handleOpenURL:url];
}
- (BOOL)application:(UIApplication*)application
openURL:(NSURL*)url
sourceApplication:(NSString*)sourceApplication
annotation:(id)annotation {
return [_lifeCycleDelegate application:application
openURL:url
sourceApplication:sourceApplication
annotation:annotation];
}
- (void)application:(UIApplication*)application
performActionForShortcutItem:(UIApplicationShortcutItem*)shortcutItem
completionHandler:(void (^)(BOOL succeeded))completionHandler {
[_lifeCycleDelegate application:application
performActionForShortcutItem:shortcutItem
completionHandler:completionHandler];
}
- (void)application:(UIApplication*)application
handleEventsForBackgroundURLSession:(nonnull NSString*)identifier
completionHandler:(nonnull void (^)(void))completionHandler {
[_lifeCycleDelegate application:application
handleEventsForBackgroundURLSession:identifier
completionHandler:completionHandler];
}
- (void)application:(UIApplication*)application
performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler {
[_lifeCycleDelegate application:application performFetchWithCompletionHandler:completionHandler];
}
- (void)addApplicationLifeCycleDelegate:(NSObject<FlutterPlugin>*)delegate {
[_lifeCycleDelegate addDelegate:delegate];
}
@end
啟動選項
#這些示例演示了使用預設啟動設定執行 Flutter。
為了自定義 Flutter 執行時,你還可以指定 Dart 入口點、庫和路由。
Dart 入口點
#在 FlutterEngine 上呼叫 run,預設情況下,會執行你的 lib/main.dart 檔案的 main() Dart 函式。
你還可以使用 runWithEntrypoint 並使用 NSString 指定不同的 Dart 函式來執行不同的入口點函式。
Dart 庫
#除了指定 Dart 函式之外,你還可以指定特定檔案中的入口點函式。
例如,以下程式碼執行 lib/other_file.dart 中的 myOtherEntrypoint(),而不是 lib/main.dart 中的 main()
flutterEngine.run(withEntrypoint: "myOtherEntrypoint", libraryURI: "other_file.dart")
[flutterEngine runWithEntrypoint:@"myOtherEntrypoint" libraryURI:@"other_file.dart"];
Route
#從 Flutter 1.22 版本開始,在構造 FlutterEngine 或 FlutterViewController 時,可以為你的 Flutter WidgetsApp 設定初始路由。
let flutterEngine = FlutterEngine()
// FlutterDefaultDartEntrypoint is the same as nil, which will run main().
engine.run(
withEntrypoint: "main", initialRoute: "/onboarding")
FlutterEngine *flutterEngine = [[FlutterEngine alloc] init];
// FlutterDefaultDartEntrypoint is the same as nil, which will run main().
[flutterEngine runWithEntrypoint:FlutterDefaultDartEntrypoint
initialRoute:@"/onboarding"];
此程式碼將你的 dart:ui 的 PlatformDispatcher.defaultRouteName 設定為 "/onboarding" 而不是 "/"。
或者,直接構造 FlutterViewController,而無需預熱 FlutterEngine
let flutterViewController = FlutterViewController(
project: nil, initialRoute: "/onboarding", nibName: nil, bundle: nil)
FlutterViewController* flutterViewController =
[[FlutterViewController alloc] initWithProject:nil
initialRoute:@"/onboarding"
nibName:nil
bundle:nil];
有關 Flutter 路由的更多資訊,請參閱 導航和路由。
其他
#前面的示例僅說明了幾種自定義 Flutter 例項啟動方式。透過 平臺通道,你可以在使用 FlutterViewController 呈現 Flutter UI 之前,自由地推送資料或以任何你喜歡的方式準備 Flutter 環境。
內容自適應檢視
#在 iOS 上,你還可以將嵌入的 FlutterView 設定為根據其內容自行調整大小。
let flutterViewController = FlutterViewController(engine: engine, nibName: nil, bundle: nil)
flutterViewController.isAutoResizable = true
_flutterViewController = [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil];
_flutterViewController.autoResizable = YES;
限制
#要使用此功能,你的根小部件必須支援無限制約束。避免在樹的頂部使用需要有界約束的小部件(如 ListView 或 LayoutBuilder),因為它們可能與動態大小調整邏輯衝突。
實際上,這意味著許多常用的小部件都不受支援,例如 ScaffoldBuilder、CupertinoTimerPicker 或任何內部依賴 LayoutBuilder 的小部件。如有疑問,你可以使用 UnconstrainedBox 測試小部件用於內容大小檢視的可用性,如以下示例所示
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context)
=> MaterialApp(home: MyPage());
}
class MyPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: UnconstrainedBox(
// TODO: Edit this line to check if a widget
// can cause problems with content-sized views.
child: Text('This works!'),
// child: Column(children: [Column(children: [Expanded(child: Text('This blows up!'))])]),
// child: ListView(children: [Text('This blows up!')]),
)
);
}
}
有關工作示例,請參閱此 示例專案。