Я пытаюсь интегрировать Flutter как модуль в приложение iOS и показать собственный экран, который у меня есть в папке ios/Runner из Flutter, с помощью PlatformView.
Итак, в качестве теста я запустил побочный проект, и из приложения Flutter показано, что собственный экран iOS довольно прост, и все работает так, как ожидалось. Затем я превратил его в модуль, добавив свойство module в файл pubspec.yaml как:
Модуль
: androidX: правда AndroidПакет: com.test.embeded iosBundleIdentifier: com.test.embeded Затем я создал новый проект собственного приложения для iOS с проектом flutter в качестве модуля, настроил FlutterEngine и т. д., но при переходе к PlatformView я получаю исключение PlatformException в Xcode:
flutter: [MAIN] -> PlatformDispatcher.instance.onError
Большое спасибо за ваше время и помощь.
Flutter как модуль.
main.dart:
import 'package:flutter/material.dart'; импортировать «пакет:native_in_fluter/home.dart»; @pragma('vm:точка входа') пустая функция() { runApp(const MyApp()); } класс MyApp расширяет StatelessWidget { const MyApp({super.key}); // Этот виджет является корнем вашего приложения. @переопределить Сборка виджета (контекст BuildContext) { вернуть MaterialApp( название: «Демо-версия Flutter», тема: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: правда, ), home: const Home (название: «Главный экран Flutter»), ); } } home.dart:
импортировать «dart:developer» как консоль; импортировать «пакет: флаттер/материал.dart»; импортировать «пакет: flutter/services.dart»; импортировать «пакет:native_in_fluter/native_viewer.dart»; импортировать «дротик: io»; импортировать «пакет: флаттер/cupertino.dart»; класс Home расширяет StatefulWidget { окончательный заголовок строки; const Home({super.key, требуется this.title}); @переопределить State createState() => _HomeState(); } класс _HomeState расширяет State { поздний канал MethodChannel; // = const MethodChannel('test_platform_channel'); @переопределить недействительный initState() { // TODO: реализовать initState супер.initState(); канал = const MethodChannel('native_viewer_platform_channel'); Channel.setMethodCallHandler((вызов) { // console.log('Home setMethodCallHandler set'); переключатель (вызов.метод) { случай 'swift_to_flutter': консоль.log( 'swift_to_flutter получен в Home с аргументами ${call.arguments}'); перерыв; по умолчанию: } вернуть вызов.аргументы; }); } @переопределить Сборка виджета (контекст BuildContext) { динамическая задняя кнопка = Платформа.isIOS ? CupertinoIcons.back : Icons.arrow_back; Карта параметры = {}; Строка ownViewTitle = ''; вернуть эшафот( AppBar: AppBar( BackgroundColor: Theme.of(context).colorScheme.inversePrimary, заголовок: Текст(виджет.заголовок), ведущий: IconButton( значок: Значок (backButton), цвет: Colors.redAccent, onPressed: () { канал.invokeMethod('закрыть'); // Navigator.pop(контекст); }), ), тело: Центр( ребенок: Столбец( mainAxisAlignment: MainAxisAlignment.center, дети: [ ElevatedButton( onPressed: () { NativeViewTitle = 'Экран А'; параметры.очистить(); параметры = { 'экран': 'viewController', 'параметр 1': 'кнопка vc A', 'параметр 2': 'кнопка vc B' }; pushNativeView( контекст: контекст, роднойViewTitle: роднойViewTitle, параметры: параметры); вызовNative(); }, дочерний элемент: const Text( «Перейти к экрану iOS A», ), ), ElevatedButton( onPressed: () { NativeViewTitle = 'Экран B'; параметры.очистить(); параметры = { «экран»: «viewController2», 'параметр 1': 'кнопка vc2 A', 'параметр 2': 'кнопка vc2 B' }; pushNativeView( контекст: контекст, роднойViewTitle: роднойViewTitle, параметры: параметры); вызовNative(); // канал.invokeMethod( // "navigate_to-screen_b", ["screen_b"]).catchError((e) { // console.log('invokeMethod b error: $e'); // }); }, дочерний элемент: const Text( «Перейти к экрану iOS B», ), ) ], ), ), ); } // недействительный вызовNative() { // Channel.invokeMethod("test", []).catchError((e) { // console.log('Ошибка теста invokeMethod: $e'); // }); // } void callNative() асинхронный { awaitchannel.invokeMethod("test", []).catchError((e) { console.log('Ошибка теста invokeMethod: $e'); }); } void pushNativeView( {требуемый контекст BuildContext, требуется строка NativeViewTitle, необходимые параметры Map}) { Навигатор.push( контекст, МатериалПажеРоут( строитель: (_) => NativeViewer( nativeScreen: "scgline uno", заголовок: роднойViewTitle, viewParams: параметры), ), ); } } Экран PlatformView:
import 'dart:io'; импортируйте «dart:developer» как консоль; импортировать «пакет: флаттер/cupertino.dart»; импортировать «пакет: флаттер/материал.dart»; импортировать «пакет: flutter/services.dart»; импортировать «пакет:native_in_fluter/adaptive_platform_view.dart»; класс NativeViewer расширяет StatefulWidget { окончательный заголовок строки; конечная строка ownScreen; окончательная Map viewParams; const NativeViewer( {супер.ключ, требуется это.title, требуется this.viewParams, требуется this.nativeScreen}); @переопределить State createState() => _NativeViewerState(); } класс _NativeViewerState расширяет State { поздний канал MethodChannel; // = const MethodChannel('test_platform_channel'); @переопределить недействительный initState() { супер.initState(); канал = const MethodChannel('native_viewer_platform_channel'); Channel.setMethodCallHandler((вызов) { console.log('Набор NativeViewer setMethodCallHandler'); переключатель (вызов.метод) { случай 'swift_to_flutter': консоль.log( 'swift_to_flutter получен в NativeViewer с аргументами ${call.arguments}'); перерыв; по умолчанию: } вернуть вызов.аргументы; }); } @переопределить Сборка виджета (контекст BuildContext) { динамическая задняя кнопка = Платформа.isIOS ? CupertinoIcons.back : Icons.arrow_back; // Это используется на стороне платформы для регистрации представления. const String viewType = ''; // Передаем параметры на сторону платформы. окончательная карта CreationParams = widget.viewParams; вернуть эшафот( AppBar: AppBar( BackgroundColor: Theme.of(context).colorScheme.inversePrimary, Название: Текст( виджет.название, // стиль: const TextStyle(fontSize: 30, цвет: Colors.white), ), ведущий: IconButton( значок: Значок (backButton), // цвет: Colors.redAccent, onPressed: () { Navigator.pop(контекст); }), ), тело: AdatptivePlatformView( родной экран: виджет.nativeScreen, тип просмотра: тип просмотра, LayoutDirection: TextDirection.ltr, СозданиеПарамс: СозданиеПарамс, CreationParamsCodec: const StandardMessageCodec(), ), ); } } адаптивный PlatformView:
import 'dart:io'; импортировать «пакет: флаттер/материал.dart»; импортировать «пакет: flutter/services.dart»; класс AdatptivePlatformView расширяет StatelessWidget { конечная строка ownScreen; окончательная строка viewType; окончательное динамическое созданиеParams; окончательное TextDirection? направление макета; окончательный MessageCodec? созданиеParamsCodec; const AdatptivePlatformView( {супер.ключ, требуется this.viewType, это.creationParams, это.creationParamsCodec, это.layoutDirection, требуется this.nativeScreen}); @переопределить Сборка виджета (контекст BuildContext) { вернуть Platform.isAndroid ? АндроидВью( тип просмотра: тип просмотра, LayoutDirection: TextDirection.ltr, СозданиеПарамс: СозданиеПарамс, CreationParamsCodec: const StandardMessageCodec(), ) : UiKitView( тип просмотра: тип просмотра, LayoutDirection: TextDirection.ltr, СозданиеПарамс: СозданиеПарамс, CreationParamsCodec: const StandardMessageCodec(), ); } } AppDelegate:
импортировать UIKit импортировать флаттер @UIApplicationMain @objc класс AppDelegate: FlutterAppDelegate { вар NativeViewerPlatformCahannel: FlutterMethodChannel? переопределить приложение func( _ приложение: UIApplication, DidFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Бул { пусть контроллер: FlutterViewController = окно?.rootViewController как! Флаттервиевконтроллер; NativeViewerPlatformCahannel = FlutterMethodChannel (имя: «native_viewer_platform_channel»,binaryMessenger: контроллер.binaryMessenger); MethodChannel?.invokeMethod("swift_to_flutter", аргументы: ["Создан экземпляр канала метода AppDelegate"]); GeneratedPluginRegistrant.register(с: self); // MARK: NATIVE в настройке Flutter слабый регистратор var = self.registrar(forPlugin: "имя-плагина"); let Factory = DynamicNativeViewFactory(messenger: registrar!.messenger()) self.registrar(forPlugin: "")!.register( фабрика, withId: "") вернуть super.application(application, DidFinishLaunchingWithOptions: launchOptions) } } Завод:
импортировать Flutter импортировать UIKit класс DynamicNativeViewFactory: NSObject, FlutterPlatformViewFactory { частный мессенджер var: FlutterBinaryMessenger init (мессенджер: FlutterBinaryMessenger) { self.messenger = мессенджер супер.инит() } функция создания( рамка withFrame: CGRect, viewIdentifier viewId: Int64, аргументы args: Есть? ) -> FlutterPlatformView { вернуть DynamicNativeView( рамка: рамка, идентификатор просмотра: идентификатор просмотра, аргументы: аргументы, binaryMessenger: мессенджер) } общественная функция createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol { вернуть FlutterStandardMessageCodec.sharedInstance() } } класс DynamicNativeView: NSObject, FlutterPlatformView { частная переменная _view: UIView = UIView(); в этом( кадр: CGRect, viewIdentifier viewId: Int64, аргументы args: Любой?, МессенджерbinaryMessenger: FlutterBinaryMessenger? ) { если let params = args как? [Строка:ЛюбойОбъект] { var screen: String = params["screen"] as! Нить; if let vc = ViewControllerGetter(rawValue: screen)?.getViewController(with: params) { _view = vc.view; } } супер.инит() } func view() -> UIView { вернуть _view } } Получатель vc:
импортировать фундамент перечисление ViewControllerGetter: String { кейс viewController случай viewController2 func getViewController (с параметрами: [String: AnyObject]?) -> UIViewController? { переключить себя { случай .viewController: если пусть vcParams = params { пусть vc = ViewController(); vc.buttonTitleA = params!["параметр 1"] as! Нить; vc.buttonTitleB = params!["параметр 2"] as! Нить; вернуть ВК } случай .viewController2: если пусть vcParams = params { пусть vc = ViewController2(); vc.buttonTitleA = params!["параметр 1"] as! Нить; vc.buttonTitleB = params!["параметр 2"] as! Нить; вернуть ВК } } вернуть ноль } } Приложение для iOS AppDelegate:
импортировать UIKit импортировать флаттер импорт FlutterPluginRegistrant @основной класс AppDelegate: UIResponder, UIApplicationDelegate { lazy var flutterEngine = FlutterEngine(name: «мой флаттер-движок»); func application(_ application: UIApplication, DidFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { flutterEngine.run(withEntrypoint: «main», LibraryURI: «package:native_in_fluter/main.dart»); GeneratedPluginRegistrant.register(с: flutterEngine); вернуть истину } // ОТМЕТКА: жизненный цикл UISceneSession приложение func (_ приложение: UIApplication, ConfigurationForConnecting ConnectionSceneSession: UISceneSession, параметры: UIScene.ConnectionOptions) -> UISceneConfiguration { // Вызывается при создании нового сеанса сцены. // Используйте этот метод, чтобы выбрать конфигурацию для создания новой сцены. return UISceneConfiguration (имя: «Конфигурация по умолчанию», sessionRole: ConnectionSceneSession.role) } func application(_ application: UIApplication, DidDiscardSceneSessions SceneSessions: Set) { // Вызывается, когда пользователь отменяет сеанс сцены. // Если какие-либо сеансы были отменены, пока приложение не работало, это будет вызвано вскоре после application:didFinishLaunchingWithOptions. // Используйте этот метод, чтобы освободить любые ресурсы, относящиеся к отброшенным сценам, поскольку они не вернутся. } } ViewController:
импортировать UIKit импортировать флаттер класс ViewController: UIViewController { переопределить функцию viewDidLoad() { супер.viewDidLoad() пусть buttonA = UIButton (тип: UIButton.ButtonType.custom) buttonA.addTarget(self, действие: #selector(showFlutterScreen), для: .touchUpInside) buttonA.setTitle("Показать флаттер", для: UIControl.State.normal) buttonA.frame = CGRect(x: 80,0, y: 210,0, ширина: 160,0, высота: 40,0) buttonA.backgroundColor = UIColor.systemGray5 buttonA.setTitleColor(UIColor.systemBlue, for: .normal)// title.tintColor = UIColor.systemBlue buttonA.layer.cornerRadius = 8; self.view.addSubview(buttonA) } @objc func showFlutterScreen() { пусть flutterEngine = (UIApplication.shared.delegate as! AppDelegate).flutterEngine; пусть flutterViewController = FlutterViewController (двигатель: flutterEngine, nibName: ноль, пакет: ноль); пусть методChannelA = FlutterMethodChannel(name: "native_viewer_platform_channel", binaryMessenger: flutterEngine.binaryMessenger); MethodChannelA.setMethodCallHandler({ //[слабое я] (вызов: FlutterMethodCall, результат: @escaping FlutterResult) -> Пустота в print("\(String (описывающий: call.method)) вызов метода, полученный на канале метода \"native_viewer_platform_channel\" с аргументами: \(String(описывающий: call.arguments))" ); переключатель (вызов.метод) { дело закрыто": self.dismissViewController(vc: flutterViewController) по умолчанию: print("Неопознанное имя метода: \(call.method)") } }) PresentViewController (VC: flutterViewController); } func PresentViewController (VC: FlutterViewController) { vc.modalPresentationStyle = .overCurrentContext; настоящее (VC, анимированное: правда); } func ignoreViewController (VC: FlutterViewController) { vc.dismiss (анимированный: правда); } }