Saltar al contenido principal

Añadir una pantalla de Flutter a una aplicación iOS

Aprende cómo añadir una única pantalla de Flutter a tu aplicación iOS existente.

Esta guía describe cómo añadir una única pantalla de Flutter a una aplicación iOS existente.

Iniciar un FlutterEngine y un FlutterViewController

#

Para lanzar una pantalla de Flutter desde una aplicación iOS existente, inicias un FlutterEngine y un FlutterViewController.

El FlutterEngine puede tener la misma vida útil que tu FlutterViewController o sobrevivir a tu FlutterViewController.

Consulta Secuencia de carga y rendimiento para obtener más análisis sobre las compensaciones de latencia y memoria al preparar previamente un motor.

Crear un FlutterEngine

#

El lugar donde crees un FlutterEngine depende de tu aplicación anfitriona.

En este ejemplo, creamos un objeto FlutterEngine dentro de un objeto Observable de SwiftUI llamado FlutterDependencies. Prepara previamente el motor llamando a run() y luego inyecta este objeto en una ContentView usando el modificador de vista environment().

MyApp.swift
swift
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)
     }
   }
}

Como ejemplo, demostramos la creación de un FlutterEngine, expuesto como una propiedad, al inicio de la aplicación en el app delegate.

AppDelegate.swift
swift
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);
  }
}

El siguiente ejemplo demuestra la creación de un FlutterEngine, expuesto como una propiedad, al inicio de la aplicación en el app delegate.

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

@interface AppDelegate : FlutterAppDelegate // More on the FlutterAppDelegate below.
@property (nonatomic,strong) FlutterEngine *flutterEngine;
@end
AppDelegate.m
objc
// 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

Mostrar un FlutterViewController con tu FlutterEngine

#

El siguiente ejemplo muestra una ContentView genérica con un NavigationLink conectado a una pantalla de Flutter. Primero, crea un FlutterViewControllerRepresentable para representar el FlutterViewController. El constructor de FlutterViewController toma el FlutterEngine previamente preparado como argumento, el cual se inyecta a través del entorno de vista.

ContentView.swift
swift
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()
      }
    }
  }
}

Ahora tienes una pantalla de Flutter embebida en tu aplicación iOS.

El siguiente ejemplo muestra un ViewController genérico con un UIButton conectado para presentar un FlutterViewController. El FlutterViewController utiliza la instancia de FlutterEngine creada en el AppDelegate.

ViewController.swift
swift
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)
  }
}

Ahora tienes una pantalla de Flutter embebida en tu aplicación iOS.

El siguiente ejemplo muestra un ViewController genérico con un UIButton conectado para presentar un FlutterViewController. El FlutterViewController utiliza la instancia de FlutterEngine creada en el AppDelegate.

ViewController.m
objc
@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

Ahora tienes una pantalla de Flutter embebida en tu aplicación iOS.

Alternativa - Crear un FlutterViewController con un FlutterEngine implícito

#

Como alternativa al ejemplo anterior, puedes dejar que el FlutterViewController cree implícitamente su propio FlutterEngine sin preparar uno con anticipación.

Por lo general, esto no se recomienda porque crear un FlutterEngine bajo demanda podría introducir una latencia notable entre el momento en que se presenta el FlutterViewController y cuando renderiza su primer frame. Sin embargo, esto podría ser útil si la pantalla de Flutter se muestra raras veces, cuando no hay buenas heurísticas para determinar cuándo debe iniciarse la Dart VM, y cuando Flutter no necesita persistir el estado entre view controllers.

Para permitir que el FlutterViewController se presente sin un FlutterEngine existente, omite la construcción de FlutterEngine y crea el FlutterViewController sin una referencia a un motor.

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
// Existing code omitted.
func showFlutter() {
  let flutterViewController = FlutterViewController(project: nil, nibName: nil, bundle: nil)
  present(flutterViewController, animated: true, completion: nil)
}
ViewController.m
objc
// Existing code omitted.
- (void)showFlutter {
  FlutterViewController *flutterViewController =
      [[FlutterViewController alloc] initWithProject:nil nibName:nil bundle:nil];
  [self presentViewController:flutterViewController animated:YES completion:nil];
}
@end

Consulta Secuencia de carga y rendimiento para obtener más exploraciones sobre la latencia y el uso de memoria.

Usar el FlutterAppDelegate

#

Se recomienda, aunque no es obligatorio, que la subclase UIApplicationDelegate de tu aplicación herede de FlutterAppDelegate.

El FlutterAppDelegate realiza funciones tales como:

  • Reenviar devoluciones de llamada de la aplicación como openURL a plugins como local_auth.
  • Mantener abierta la conexión de Flutter en modo de depuración cuando se bloquea la pantalla del teléfono.

Crear una subclase de FlutterAppDelegate

#

La creación de una subclase de FlutterAppDelegate en aplicaciones UIKit se mostró en la sección Iniciar un FlutterEngine y un FlutterViewController. En una aplicación SwiftUI, puedes crear una subclase de FlutterAppDelegate y anotarla con la macro Observable() de la siguiente manera:

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 {
      // 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()
      }
  }
}

Luego, en tu vista, el AppDelegate es accesible a través del entorno de la vista.

ContentView.swift
swift
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()
      }
    }
  }
}

Si no puedes heredar directamente de FlutterAppDelegate

#

Si tu app delegate no puede heredar directamente de FlutterAppDelegate, haz que tu app delegate implemente el protocolo FlutterAppLifeCycleProvider para asegurarte de que tus plugins reciban las devoluciones de llamada necesarias. De lo contrario, los plugins que dependan de estos eventos podrían tener un comportamiento indefinido.

Por ejemplo:

AppDelegate.swift
swift
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)
  }
}
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

La implementación debería delegar principalmente en un 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];
}

// 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

Opciones de lanzamiento

#

Los ejemplos demuestran cómo ejecutar Flutter utilizando la configuración de lanzamiento predeterminada.

Para personalizar tu tiempo de ejecución de Flutter, también puedes especificar el punto de entrada, la biblioteca y la ruta de Dart.

Punto de entrada de Dart

#

Llamar a run en un FlutterEngine, por defecto, ejecuta la función Dart main() de tu archivo lib/main.dart.

También puedes ejecutar una función de punto de entrada diferente utilizando runWithEntrypoint con un NSString que especifique una función Dart diferente.

Biblioteca de Dart

#

Además de especificar una función Dart, puedes especificar una función de punto de entrada en un archivo específico.

Por ejemplo, lo siguiente ejecuta myOtherEntrypoint() en lib/other_file.dart en lugar de main() en lib/main.dart:

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

Ruta

#

A partir de la versión 1.22 de Flutter, se puede establecer una ruta inicial para tu WidgetsApp de Flutter al construir el FlutterEngine o el FlutterViewController.

swift
let flutterEngine = FlutterEngine()
// FlutterDefaultDartEntrypoint is the same as nil, which will run main().
engine.run(
  withEntrypoint: "main", initialRoute: "/onboarding")
objc
FlutterEngine *flutterEngine = [[FlutterEngine alloc] init];
// FlutterDefaultDartEntrypoint is the same as nil, which will run main().
[flutterEngine runWithEntrypoint:FlutterDefaultDartEntrypoint
                    initialRoute:@"/onboarding"];

Este código establece el valor PlatformDispatcher.defaultRouteName de tu dart:ui a "/onboarding" en lugar de "/".

Alternativamente, para construir un FlutterViewController directamente sin preparar previamente un 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];

Consulta Navegación y enrutamiento para obtener más información sobre las rutas de Flutter.

Otro

#

El ejemplo anterior solo ilustra algunas formas de personalizar cómo se inicia una instancia de Flutter. Utilizando los canales de plataforma, eres libre de enviar datos o preparar tu entorno de Flutter de la manera que desees, antes de presentar la interfaz de usuario de Flutter usando un FlutterViewController.

Vistas ajustadas al contenido

#

En iOS, también puedes configurar tu FlutterView integrada para que se ajuste al tamaño de su contenido.

swift
let flutterViewController = FlutterViewController(engine: engine, nibName: nil, bundle: nil)
flutterViewController.isAutoResizable = true
objc
_flutterViewController = [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil];
_flutterViewController.autoResizable = YES;

Restricciones

#

Para utilizar esto, tu widget raíz debe admitir restricciones no delimitadas (unbounded constraints). Evita el uso de widgets que requieran restricciones delimitadas (como ListView o LayoutBuilder) en la parte superior de tu árbol, ya que pueden entrar en conflicto con la lógica de dimensionamiento dinámico.

En la práctica, esto significa que bastantes widgets comunes no son compatibles, tales como ScaffoldBuilder, CupertinoTimerPicker, o cualquier widget que dependa internamente de un LayoutBuilder. En caso de duda, puedes usar un UnconstrainedBox para probar la usabilidad de un widget para una vista ajustada al contenido, como en el siguiente ejemplo:

dart
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!')]),
        )
    );
  }
}

Para ver un ejemplo funcional, consulta este proyecto de ejemplo.