Skip to main content

向 iOS 应用添加 Flutter 屏幕

本指南介绍如何将单个 Flutter 屏幕添加到现有的 iOS 应用中。

启动 FlutterEngine 和 FlutterViewController

#

要从现有的 iOS 应用启动 Flutter 屏幕,您需要启动一个 FlutterEngine 和一个 FlutterViewController

FlutterEngine 的生命周期可能与您的 FlutterViewController 相同,也可能比您的 FlutterViewController 更长。

有关预热引擎的延迟和内存权衡的更多分析,请参阅加载顺序和性能

创建 FlutterEngine

#

您创建 FlutterEngine 的位置取决于您的宿主应用程序。

在此示例中,我们在名为 FlutterDependencies 的 SwiftUI Observable 对象内创建一个 FlutterEngine 对象。通过调用 run() 预热引擎,然后使用 environment() 视图修饰符将此对象注入到 ContentView 中。

MyApp.swift
swift
import SwiftUI
import Flutter
// 下面的库将插件与 iOS 平台代码连接到此应用程序。
import FlutterPluginRegistrant

@Observable
class FlutterDependencies {
  let flutterEngine = FlutterEngine(name: "my flutter engine")
  init() {
    // 使用默认的 Flutter 路由运行默认的 Dart 入口点。
    flutterEngine.run()
    // 将插件与 iOS 平台代码连接到此应用程序。
    GeneratedPluginRegistrant.register(with: self.flutterEngine);
  }
}

@main
struct MyApp: App {
    // flutterDependencies 将通过视图环境注入。
    @State var flutterDependencies = FlutterDependencies()
    var body: some Scene {
      WindowGroup {
        ContentView()
          .environment(flutterDependencies)
      }
    }
}

例如,我们在应用程序委托的应用程序启动时演示创建 FlutterEngine,并将其公开为属性。

AppDelegate.swift
swift
import UIKit
import Flutter
// 下面的库将插件与 iOS 平台代码连接到此应用程序。
import FlutterPluginRegistrant

@UIApplicationMain
class AppDelegate: FlutterAppDelegate { // 关于 FlutterAppDelegate 的更多信息。
  lazy var flutterEngine = FlutterEngine(name: "my flutter engine")

  override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // 使用默认的 Flutter 路由运行默认的 Dart 入口点。
    flutterEngine.run();
    // 将插件与 iOS 平台代码连接到此应用程序。
    GeneratedPluginRegistrant.register(with: self.flutterEngine);
    return super.application(application, didFinishLaunchingWithOptions: launchOptions);
  }
}

以下示例演示了在应用程序委托的应用程序启动时创建 FlutterEngine,并将其公开为属性。

AppDelegate.h
objc
@import UIKit;
@import Flutter;

@interface AppDelegate : FlutterAppDelegate // 关于 FlutterAppDelegate 的更多信息,请参见下文。
@property (nonatomic,strong) FlutterEngine *flutterEngine;
@end
AppDelegate.m
objc
// 下面的库将插件与 iOS 平台代码连接到此应用程序。
#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"];
  // 使用默认的 Flutter 路由运行默认的 Dart 入口点。
  [self.flutterEngine run];
  // 将插件与 iOS 平台代码连接到此应用程序。
  [GeneratedPluginRegistrant registerWithRegistry:self.flutterEngine];
  return [super application:application didFinishLaunchingWithOptions:launchOptions];
}

@end

使用 FlutterEngine 显示 FlutterViewController

#

以下示例显示了一个通用的 ContentView,其中包含一个连接到 Flutter 屏幕的 NavigationLink。首先,创建一个 FlutterViewControllerRepresentable 来表示 FlutterViewControllerFlutterViewController 构造函数将预热的 FlutterEngine 作为参数,该参数通过视图环境注入。

ContentView.swift
swift
import SwiftUI
import Flutter

struct FlutterViewControllerRepresentable: UIViewControllerRepresentable {
  // Flutter 依赖项通过视图环境传入。
  @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()
      }
    }
  }
}

现在,您已将 Flutter 屏幕嵌入到 iOS 应用中。

以下示例显示了一个通用的 ViewController,其中包含一个连接到呈现 FlutterViewControllerUIButtonFlutterViewController 使用在 AppDelegate 中创建的 FlutterEngine 实例。

ViewController.swift
swift
import UIKit
import Flutter

class ViewController: UIViewController {
  override func viewDidLoad() {
    super.viewDidLoad()

    // 创建一个按钮,按下时调用 showFlutter 函数。
    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)
  }
}

现在,您已将 Flutter 屏幕嵌入到 iOS 应用中。

以下示例显示了一个通用的 ViewController,其中包含一个连接到呈现 FlutterViewControllerUIButtonFlutterViewController 使用在 AppDelegate 中创建的 FlutterEngine 实例。

ViewController.m
objc
@import Flutter;
#import "AppDelegate.h"
#import "ViewController.h"

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];

    // 创建一个按钮,按下时调用 showFlutter 函数。
    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

现在,您已将 Flutter 屏幕嵌入到 iOS 应用中。

或者 - 使用隐式 FlutterEngine 创建 FlutterViewController

#

作为前面示例的替代方法,您可以让 FlutterViewController 隐式创建自己的 FlutterEngine,而无需提前预热。

这通常不推荐,因为按需创建 FlutterEngine 可能会在呈现 FlutterViewController 和渲染其第一帧之间引入明显的延迟。但是,如果很少显示 Flutter 屏幕,没有好的启发式方法来确定何时应启动 Dart VM,并且 Flutter 不需要在视图控制器之间保持状态,这可能会很有用。

要让 FlutterViewController 在没有现有 FlutterEngine 的情况下呈现,请省略 FlutterEngine 的构造,并创建一个没有引擎引用的 FlutterViewController

ContentView.swift
swift
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()
      }
    }
  }
}
ViewController.swift
swift
// 省略现有代码。
func showFlutter() {
  let flutterViewController = FlutterViewController(project: nil, nibName: nil, bundle: nil)
  present(flutterViewController, animated: true, completion: nil)
}
ViewController.m
objc
// 省略现有代码。
- (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() 宏对其进行注释,如下所示:

MyApp.swift
swift
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 {
      // 使用默认的 Flutter 路由运行默认的 Dart 入口点。
      flutterEngine.run();
      // 用于连接插件(只有当您拥有带有 iOS 平台代码的插件时)。
      GeneratedPluginRegistrant.register(with: self.flutterEngine);
      return true;
    }
}

@main
struct MyApp: App {
  // 使用此属性包装器告诉 SwiftUI
  // 它应该为应用程序委托使用 AppDelegate 类
  @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

  var body: some Scene {
      WindowGroup {
        ContentView()
      }
  }
}

然后,在您的视图中,可以通过视图环境访问 AppDelegate

ContentView.swift
swift
import SwiftUI
import Flutter

struct FlutterViewControllerRepresentable: UIViewControllerRepresentable {
  // 通过视图环境访问 AppDelegate。
  @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 协议,以确保您的插件收到必要的回调。否则,依赖于这些事件的插件可能会出现未定义的行为。

例如:

AppDelegate.swift
swift
import Foundation
import Flutter

@Observable
class AppDelegate: UIResponder, UIApplicationDelegate, FlutterAppLifeCycleProvider {

  private let lifecycleDelegate = FlutterPluginAppLifeCycleDelegate()

  let flutterEngine = FlutterEngine(name: "my flutter engine")

  override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
  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)
  }
}
AppDelegate.h
objc
@import Flutter;
@import UIKit;
@import FlutterPluginRegistrant;

@interface AppDelegate : UIResponder <UIApplicationDelegate, FlutterAppLifeCycleProvider>
@property (strong, nonatomic) UIWindow *window;
@property (nonatomic,strong) FlutterEngine *flutterEngine;
@end

实现应该主要委托给 FlutterPluginAppLifeCycleDelegate

AppDelegate.m
objc
@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];
}

// 返回关键窗口的 rootViewController,如果它是 FlutterViewController。
// 否则,返回 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 函数。

您还可以使用带有指定不同 Dart 函数的 NSStringrunWithEntrypoint 来运行不同的入口点函数。

Dart 库

#

除了指定 Dart 函数外,您还可以指定特定文件中的入口点函数。

例如,以下代码运行 lib/other_file.dart 中的 myOtherEntrypoint(),而不是 lib/main.dart 中的 main()

swift
flutterEngine.run(withEntrypoint: "myOtherEntrypoint", libraryURI: "other_file.dart")
objc
[flutterEngine runWithEntrypoint:@"myOtherEntrypoint" libraryURI:@"other_file.dart"];

路由

#

从 Flutter 1.22 版本开始,在构建 FlutterEngine 或 FlutterViewController 时,可以为您的 Flutter WidgetsApp 设置初始路由。

swift
let flutterEngine = FlutterEngine()
// FlutterDefaultDartEntrypoint 与 nil 相同,它将运行 main()。
engine.run(
  withEntrypoint: "main", initialRoute: "/onboarding")
objc
FlutterEngine *flutterEngine = [[FlutterEngine alloc] init];
// FlutterDefaultDartEntrypoint 与 nil 相同,它将运行 main()。
[flutterEngine runWithEntrypoint:FlutterDefaultDartEntrypoint
                    initialRoute:@"/onboarding"];

这段代码将您的 dart:uiPlatformDispatcher.defaultRouteName 设置为 "/onboarding",而不是 "/"

或者,要直接构建 FlutterViewController 而无需预热 FlutterEngine:

swift
let flutterViewController = FlutterViewController(
      project: nil, initialRoute: "/onboarding", nibName: nil, bundle: nil)
objc
FlutterViewController* flutterViewController =
      [[FlutterViewController alloc] initWithProject:nil
                                        initialRoute:@"/onboarding"
                                             nibName:nil
                                              bundle:nil];

有关 Flutter 路由的更多信息,请参阅导航和路由

其他

#

前面的示例仅说明了自定义 Flutter 实例初始化方式的几种方法。使用平台通道,您可以自由地推送数据或以任何您想要的方式准备 Flutter 环境,然后使用 FlutterViewController 显示 Flutter UI。