From e7542fb5900e9b178ba7a2b915ce99853d76e69b Mon Sep 17 00:00:00 2001 From: timbotimbo Date: Sun, 3 Aug 2025 15:51:48 +0200 Subject: [PATCH 1/7] Handle most lint warnings in the plugin. (flutter_lints 6.0.0) --- lib/flutter_unity_widget.dart | 2 +- lib/src/facade_controller.dart | 2 +- lib/src/facade_widget.dart | 2 +- lib/src/helpers/events.dart | 15 +++++++-------- lib/src/helpers/misc.dart | 7 ++++--- lib/src/io/device_method.dart | 5 +++-- lib/src/io/mobile_unity_widget_controller.dart | 13 +++++++++++++ lib/src/io/unity_widget.dart | 2 +- lib/src/io/windows_unity_widget_view.dart | 2 +- lib/src/web/unity_widget.dart | 2 +- lib/src/web/web_unity_widget_controller.dart | 13 +++++++------ lib/src/web/web_unity_widget_view.dart | 4 ++-- test/fake_unity_widget_controllers.dart | 6 +++--- 13 files changed, 45 insertions(+), 30 deletions(-) diff --git a/lib/flutter_unity_widget.dart b/lib/flutter_unity_widget.dart index 3390c5808..61f57d84e 100755 --- a/lib/flutter_unity_widget.dart +++ b/lib/flutter_unity_widget.dart @@ -1,4 +1,4 @@ -library flutter_unity_widget; +library; export 'src/facade_controller.dart'; export 'src/facade_widget.dart' diff --git a/lib/src/facade_controller.dart b/lib/src/facade_controller.dart index d4651b6f5..dc9af5bec 100755 --- a/lib/src/facade_controller.dart +++ b/lib/src/facade_controller.dart @@ -1,4 +1,4 @@ -typedef void UnityCreatedCallback(UnityWidgetController controller); +typedef UnityCreatedCallback = void Function(UnityWidgetController controller); abstract class UnityWidgetController { static dynamic webRegistrar; diff --git a/lib/src/facade_widget.dart b/lib/src/facade_widget.dart index d9b6e006d..f83118fcf 100755 --- a/lib/src/facade_widget.dart +++ b/lib/src/facade_widget.dart @@ -77,7 +77,7 @@ class UnityWidget extends StatefulWidget { final TextDirection? layoutDirection; @override - _UnityWidgetState createState() => _UnityWidgetState(); + State createState() => _UnityWidgetState(); } class _UnityWidgetState extends State { diff --git a/lib/src/helpers/events.dart b/lib/src/helpers/events.dart index 46f4b16b0..444835dd3 100755 --- a/lib/src/helpers/events.dart +++ b/lib/src/helpers/events.dart @@ -5,32 +5,31 @@ class UnityEvent { final int unityId; /// The value wrapped by this event - final T value; + final T? value; /// Build a Unity Event, that relates a mapId with a given value. /// /// The `unityId` is the id of the map that triggered the event. /// `value` may be `null` in events that don't transport any meaningful data. - UnityEvent(this.unityId, this.value); + UnityEvent(this.unityId, [this.value]); } class UnitySceneLoadedEvent extends UnityEvent { - UnitySceneLoadedEvent(int unityId, SceneLoaded? value) - : super(unityId, value); + UnitySceneLoadedEvent(super.unityId, super.value); } class UnityLoadedEvent extends UnityEvent { - UnityLoadedEvent(int unityId, void value) : super(unityId, value); + UnityLoadedEvent(super.unityId); } class UnityUnLoadedEvent extends UnityEvent { - UnityUnLoadedEvent(int unityId, void value) : super(unityId, value); + UnityUnLoadedEvent(super.unityId); } class UnityCreatedEvent extends UnityEvent { - UnityCreatedEvent(int unityId, void value) : super(unityId, value); + UnityCreatedEvent(super.unityId); } class UnityMessageEvent extends UnityEvent { - UnityMessageEvent(int unityId, dynamic value) : super(unityId, value); + UnityMessageEvent(super.unityId, super.value); } diff --git a/lib/src/helpers/misc.dart b/lib/src/helpers/misc.dart index dc9bb60a6..7993f23dc 100644 --- a/lib/src/helpers/misc.dart +++ b/lib/src/helpers/misc.dart @@ -12,6 +12,7 @@ class UnknownUnityIDError extends Error { /// Message describing the assertion error. final Object? message; + @override String toString() { if (message != null) { return "Unknown unity ID $unityId: ${Error.safeToString(message)}"; @@ -20,8 +21,8 @@ class UnknownUnityIDError extends Error { } } -typedef void UnityMessageCallback(dynamic handler); +typedef UnityMessageCallback = void Function(dynamic handler); -typedef void UnitySceneChangeCallback(SceneLoaded? message); +typedef UnitySceneChangeCallback = void Function(SceneLoaded? message); -typedef void UnityUnloadCallback(); +typedef UnityUnloadCallback = void Function(); diff --git a/lib/src/io/device_method.dart b/lib/src/io/device_method.dart index 997614f0d..063ed7623 100644 --- a/lib/src/io/device_method.dart +++ b/lib/src/io/device_method.dart @@ -86,14 +86,14 @@ class MethodChannelUnityWidget extends UnityWidgetPlatform { _unityStreamController.add(UnityMessageEvent(unityId, call.arguments)); break; case "events#onUnityUnloaded": - _unityStreamController.add(UnityLoadedEvent(unityId, call.arguments)); + _unityStreamController.add(UnityLoadedEvent(unityId)); break; case "events#onUnitySceneLoaded": _unityStreamController.add(UnitySceneLoadedEvent( unityId, SceneLoaded.fromMap(call.arguments))); break; case "events#onUnityCreated": - _unityStreamController.add(UnityCreatedEvent(unityId, call.arguments)); + _unityStreamController.add(UnityCreatedEvent(unityId)); break; default: throw UnimplementedError("Unimplemented ${call.method} method"); @@ -158,6 +158,7 @@ class MethodChannelUnityWidget extends UnityWidgetPlatform { bool? unityWebSource, String? unitySrcUrl, }) { + // ignore: no_leading_underscores_for_local_identifiers final String _viewType = 'plugin.xraph.com/unity_view'; if (useAndroidViewSurf != null) useAndroidViewSurface = useAndroidViewSurf; diff --git a/lib/src/io/mobile_unity_widget_controller.dart b/lib/src/io/mobile_unity_widget_controller.dart index 0603969ee..f362915a1 100644 --- a/lib/src/io/mobile_unity_widget_controller.dart +++ b/lib/src/io/mobile_unity_widget_controller.dart @@ -70,6 +70,7 @@ class MobileUnityWidgetController extends UnityWidgetController { /// Checks to see if unity player is ready to be used /// Returns `true` if unity player is ready. + @override Future? isReady() { if (!_unityWidgetState.widget.enablePlaceholder) { return UnityWidgetPlatform.instance.isReady(unityId: unityId); @@ -79,6 +80,7 @@ class MobileUnityWidgetController extends UnityWidgetController { /// Get the current pause state of the unity player /// Returns `true` if unity player is paused. + @override Future? isPaused() { if (!_unityWidgetState.widget.enablePlaceholder) { return UnityWidgetPlatform.instance.isPaused(unityId: unityId); @@ -88,6 +90,7 @@ class MobileUnityWidgetController extends UnityWidgetController { /// Get the current load state of the unity player /// Returns `true` if unity player is loaded. + @override Future? isLoaded() { if (!_unityWidgetState.widget.enablePlaceholder) { return UnityWidgetPlatform.instance.isLoaded(unityId: unityId); @@ -97,6 +100,7 @@ class MobileUnityWidgetController extends UnityWidgetController { /// Helper method to know if Unity has been put in background mode (WIP) unstable /// Returns `true` if unity player is in background. + @override Future? inBackground() { if (!_unityWidgetState.widget.enablePlaceholder) { return UnityWidgetPlatform.instance.inBackground(unityId: unityId); @@ -107,6 +111,7 @@ class MobileUnityWidgetController extends UnityWidgetController { /// Creates a unity player if it's not already created. Please only call this if unity is not ready, /// or is in unloaded state. Use [isLoaded] to check. /// Returns `true` if unity player was created succesfully. + @override Future? create() { if (!_unityWidgetState.widget.enablePlaceholder) { return UnityWidgetPlatform.instance.createUnityPlayer(unityId: unityId); @@ -121,6 +126,7 @@ class MobileUnityWidgetController extends UnityWidgetController { /// ```dart /// postMessage("GameManager", "openScene", "ThirdScene") /// ``` + @override Future? postMessage(String gameObject, methodName, message) { if (!_unityWidgetState.widget.enablePlaceholder) { return UnityWidgetPlatform.instance.postMessage( @@ -140,6 +146,7 @@ class MobileUnityWidgetController extends UnityWidgetController { /// ```dart /// postJsonMessage("GameManager", "openScene", {"buildIndex": 3, "name": "ThirdScene"}) /// ``` + @override Future? postJsonMessage( String gameObject, String methodName, Map message) { if (!_unityWidgetState.widget.enablePlaceholder) { @@ -154,6 +161,7 @@ class MobileUnityWidgetController extends UnityWidgetController { } /// Pause the unity in-game player with this method + @override Future? pause() { if (!_unityWidgetState.widget.enablePlaceholder) { return UnityWidgetPlatform.instance.pausePlayer(unityId: unityId); @@ -162,6 +170,7 @@ class MobileUnityWidgetController extends UnityWidgetController { } /// Resume the unity in-game player with this method idf it is in a paused state + @override Future? resume() { if (!_unityWidgetState.widget.enablePlaceholder) { return UnityWidgetPlatform.instance.resumePlayer(unityId: unityId); @@ -171,6 +180,7 @@ class MobileUnityWidgetController extends UnityWidgetController { /// Sometimes you want to open unity in it's own process and openInNativeProcess does just that. /// It works for Android and iOS is WIP + @override Future? openInNativeProcess() { if (!_unityWidgetState.widget.enablePlaceholder) { return UnityWidgetPlatform.instance.openInNativeProcess(unityId: unityId); @@ -180,6 +190,7 @@ class MobileUnityWidgetController extends UnityWidgetController { /// Unloads unity player from th current process (Works on Android only for now) /// iOS is WIP. For more information please read [Unity Docs](https://docs.unity3d.com/2020.2/Documentation/Manual/UnityasaLibrary.html) + @override Future? unload() { if (!_unityWidgetState.widget.enablePlaceholder) { return UnityWidgetPlatform.instance.unloadPlayer(unityId: unityId); @@ -188,6 +199,7 @@ class MobileUnityWidgetController extends UnityWidgetController { } /// Quits unity player. Note that this kills the current flutter process, thus quiting the app + @override Future? quit() { if (!_unityWidgetState.widget.enablePlaceholder) { return UnityWidgetPlatform.instance.quitPlayer(unityId: unityId); @@ -206,6 +218,7 @@ class MobileUnityWidgetController extends UnityWidgetController { _onUnityUnloadedSub = null; } + @override void dispose() { _cancelSubscriptions(); UnityWidgetPlatform.instance.dispose(unityId: unityId); diff --git a/lib/src/io/unity_widget.dart b/lib/src/io/unity_widget.dart index 508ccf069..69ed20caf 100644 --- a/lib/src/io/unity_widget.dart +++ b/lib/src/io/unity_widget.dart @@ -126,7 +126,7 @@ class UnityWidget extends StatefulWidget { final TextDirection? layoutDirection; @override - _UnityWidgetState createState() => _UnityWidgetState(); + State createState() => _UnityWidgetState(); } class _UnityWidgetState extends State { diff --git a/lib/src/io/windows_unity_widget_view.dart b/lib/src/io/windows_unity_widget_view.dart index 2a6862aac..49f518d40 100644 --- a/lib/src/io/windows_unity_widget_view.dart +++ b/lib/src/io/windows_unity_widget_view.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; class WindowsUnityWidgetView extends StatefulWidget { - const WindowsUnityWidgetView({Key? key}) : super(key: key); + const WindowsUnityWidgetView({super.key}); @override State createState() => _WindowsUnityWidgetViewState(); diff --git a/lib/src/web/unity_widget.dart b/lib/src/web/unity_widget.dart index 5593aaf41..379124706 100755 --- a/lib/src/web/unity_widget.dart +++ b/lib/src/web/unity_widget.dart @@ -81,7 +81,7 @@ class UnityWidget extends StatefulWidget { final TextDirection? layoutDirection; @override - _UnityWidgetState createState() => _UnityWidgetState(); + State createState() => _UnityWidgetState(); } typedef WebUnityWidgetState = _UnityWidgetState; diff --git a/lib/src/web/web_unity_widget_controller.dart b/lib/src/web/web_unity_widget_controller.dart index 7485e5c7c..cf4586d8a 100644 --- a/lib/src/web/web_unity_widget_controller.dart +++ b/lib/src/web/web_unity_widget_controller.dart @@ -101,7 +101,7 @@ class WebUnityWidgetController extends UnityWidgetController { return channel; } - _registerEvents() { + void _registerEvents() { if (kIsWeb) { _messageListener = ((web.Event event) { if (event is web.MessageEvent) { @@ -126,7 +126,7 @@ class WebUnityWidgetController extends UnityWidgetController { if (data == 'unityReady') { unityReady = true; unityPause = false; - _unityStreamController.add(UnityCreatedEvent(0, {})); + _unityStreamController.add(UnityCreatedEvent(0)); return; } else { try { @@ -218,13 +218,13 @@ class WebUnityWidgetController extends UnityWidgetController { void callUnityFn({required String fnName}) { if (kIsWeb) { - final web.MessageEvent _unityFlutterBidingFn = web.MessageEvent( + final web.MessageEvent unityFlutterBidingFn = web.MessageEvent( 'unityFlutterBidingFnCal', web.MessageEventInit( data: fnName.toJS, ), ); - web.window.dispatchEvent(_unityFlutterBidingFn); + web.window.dispatchEvent(unityFlutterBidingFn); } } @@ -234,7 +234,7 @@ class WebUnityWidgetController extends UnityWidgetController { required String message, }) { if (kIsWeb) { - final web.MessageEvent _unityFlutterBiding = web.MessageEvent( + final web.MessageEvent unityFlutterBiding = web.MessageEvent( 'unityFlutterBiding', web.MessageEventInit( data: json.encode({ @@ -244,7 +244,7 @@ class WebUnityWidgetController extends UnityWidgetController { }).toJS, ), ); - web.window.dispatchEvent(_unityFlutterBiding); + web.window.dispatchEvent(unityFlutterBiding); postProcess(); } } @@ -322,6 +322,7 @@ class WebUnityWidgetController extends UnityWidgetController { _onUnityUnloadedSub = null; } + @override void dispose() { _cancelSubscriptions(); if (kIsWeb) { diff --git a/lib/src/web/web_unity_widget_view.dart b/lib/src/web/web_unity_widget_view.dart index 28f5c23d9..fbfadb544 100644 --- a/lib/src/web/web_unity_widget_view.dart +++ b/lib/src/web/web_unity_widget_view.dart @@ -5,10 +5,10 @@ import 'package:webview_flutter_web/webview_flutter_web.dart'; // used indirectl class WebUnityWidgetView extends StatefulWidget { const WebUnityWidgetView({ - Key? key, + super.key, required this.onWebViewCreated, required this.unityOptions, - }) : super(key: key); + }); final Map unityOptions; final void Function() onWebViewCreated; diff --git a/test/fake_unity_widget_controllers.dart b/test/fake_unity_widget_controllers.dart index f6a61403d..ab1b5c210 100644 --- a/test/fake_unity_widget_controllers.dart +++ b/test/fake_unity_widget_controllers.dart @@ -16,9 +16,9 @@ class FakePlatformUnityWidget { bool playerCreated = false; bool playerUnloaded = false; bool unityReady = false; - bool? unityPaused = null; - bool? unityInBackground = null; - List> _sentMessages = []; + bool? unityPaused; + bool? unityInBackground; + final List> _sentMessages = []; Future onMethodCall(MethodCall call) { switch (call.method) { From 9aab33d4313b73093e00d6221a313feba8462506 Mon Sep 17 00:00:00 2001 From: timbotimbo Date: Sun, 3 Aug 2025 16:23:52 +0200 Subject: [PATCH 2/7] Fix lint warnings in example. --- example/lib/main.dart | 2 +- example/lib/menu_screen.dart | 2 +- example/lib/screens/api_screen.dart | 6 +++--- example/lib/screens/loader_screen.dart | 6 +++--- example/lib/screens/no_interaction_screen.dart | 10 +++------- example/lib/screens/orientation_screen.dart | 6 +++--- example/lib/screens/simple_screen.dart | 10 +++------- 7 files changed, 17 insertions(+), 25 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 19a8f82b0..021fd6c1a 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -12,7 +12,7 @@ void main() { } class MyApp extends StatelessWidget { - const MyApp({Key? key}) : super(key: key); + const MyApp({super.key}); // This widget is the root of your application. @override diff --git a/example/lib/menu_screen.dart b/example/lib/menu_screen.dart index c5809afed..287027949 100644 --- a/example/lib/menu_screen.dart +++ b/example/lib/menu_screen.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; class MenuScreen extends StatefulWidget { - const MenuScreen({Key? key}) : super(key: key); + const MenuScreen({super.key}); @override State createState() => _MenuScreenState(); diff --git a/example/lib/screens/api_screen.dart b/example/lib/screens/api_screen.dart index 9a5ea6ebd..9fb7dc28e 100644 --- a/example/lib/screens/api_screen.dart +++ b/example/lib/screens/api_screen.dart @@ -5,7 +5,7 @@ import 'package:flutter_unity_widget/flutter_unity_widget.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; class ApiScreen extends StatefulWidget { - const ApiScreen({Key? key}) : super(key: key); + const ApiScreen({super.key}); @override State createState() => _ApiScreenState(); @@ -148,7 +148,7 @@ class _ApiScreenState extends State { ); } - void onUnityMessage(message) { + void onUnityMessage(dynamic message) { print('Received message from unity: ${message.toString()}'); } @@ -162,7 +162,7 @@ class _ApiScreenState extends State { } // Callback that connects the created controller to the unity controller - void onUnityCreated(controller) { + void onUnityCreated(UnityWidgetController controller) { _unityWidgetController = controller; } } diff --git a/example/lib/screens/loader_screen.dart b/example/lib/screens/loader_screen.dart index e5034ef21..909eddac3 100644 --- a/example/lib/screens/loader_screen.dart +++ b/example/lib/screens/loader_screen.dart @@ -5,7 +5,7 @@ import 'package:flutter_unity_widget/flutter_unity_widget.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; class LoaderScreen extends StatefulWidget { - const LoaderScreen({Key? key}) : super(key: key); + const LoaderScreen({super.key}); @override State createState() => _LoaderScreenState(); @@ -82,12 +82,12 @@ class _LoaderScreenState extends State { ); } - void onUnityMessage(message) { + void onUnityMessage(dynamic message) { print('Received message from unity: ${message.toString()}'); } // Callback that connects the created controller to the unity controller - void onUnityCreated(controller) { + void onUnityCreated(UnityWidgetController controller) { _unityWidgetController = controller; } } diff --git a/example/lib/screens/no_interaction_screen.dart b/example/lib/screens/no_interaction_screen.dart index fb92a5609..42881a6d3 100644 --- a/example/lib/screens/no_interaction_screen.dart +++ b/example/lib/screens/no_interaction_screen.dart @@ -3,16 +3,13 @@ import 'package:flutter_unity_widget/flutter_unity_widget.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; class NoInteractionScreen extends StatefulWidget { - const NoInteractionScreen({Key? key}) : super(key: key); + const NoInteractionScreen({super.key}); @override State createState() => _NoInteractionScreenState(); } class _NoInteractionScreenState extends State { - static final GlobalKey _scaffoldKey = - GlobalKey(); - UnityWidgetController? _unityWidgetController; @override @@ -29,7 +26,6 @@ class _NoInteractionScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - key: _scaffoldKey, appBar: AppBar( title: const Text('No Interaction Screen'), ), @@ -75,7 +71,7 @@ class _NoInteractionScreenState extends State { ); } - void onUnityMessage(message) { + void onUnityMessage(dynamic message) { print('Received message from unity: ${message.toString()}'); } @@ -89,7 +85,7 @@ class _NoInteractionScreenState extends State { } // Callback that connects the created controller to the unity controller - void _onUnityCreated(controller) { + void _onUnityCreated(UnityWidgetController controller) { controller.resume(); _unityWidgetController = controller; } diff --git a/example/lib/screens/orientation_screen.dart b/example/lib/screens/orientation_screen.dart index df3270392..0bee44516 100644 --- a/example/lib/screens/orientation_screen.dart +++ b/example/lib/screens/orientation_screen.dart @@ -4,7 +4,7 @@ import 'package:flutter_unity_widget/flutter_unity_widget.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; class OrientationScreen extends StatefulWidget { - const OrientationScreen({Key? key}) : super(key: key); + const OrientationScreen({super.key}); @override State createState() => _OrientationScreenState(); @@ -97,12 +97,12 @@ class _OrientationScreenState extends State { ); } - void onUnityMessage(message) { + void onUnityMessage(dynamic message) { print('Received message from unity: ${message.toString()}'); } // Callback that connects the created controller to the unity controller - void onUnityCreated(controller) { + void onUnityCreated(UnityWidgetController controller) { _unityWidgetController = controller; } } diff --git a/example/lib/screens/simple_screen.dart b/example/lib/screens/simple_screen.dart index 4aa5aa605..9965a37f5 100644 --- a/example/lib/screens/simple_screen.dart +++ b/example/lib/screens/simple_screen.dart @@ -3,16 +3,13 @@ import 'package:flutter_unity_widget/flutter_unity_widget.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; class SimpleScreen extends StatefulWidget { - const SimpleScreen({Key? key}) : super(key: key); + const SimpleScreen({super.key}); @override State createState() => _SimpleScreenState(); } class _SimpleScreenState extends State { - static final GlobalKey _scaffoldKey = - GlobalKey(); - UnityWidgetController? _unityWidgetController; double _sliderValue = 0.0; @@ -30,7 +27,6 @@ class _SimpleScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - key: _scaffoldKey, appBar: AppBar( title: const Text('Simple Screen'), ), @@ -91,7 +87,7 @@ class _SimpleScreenState extends State { ); } - void onUnityMessage(message) { + void onUnityMessage(dynamic message) { print('Received message from unity: ${message.toString()}'); } @@ -105,7 +101,7 @@ class _SimpleScreenState extends State { } // Callback that connects the created controller to the unity controller - void _onUnityCreated(controller) { + void _onUnityCreated(UnityWidgetController controller) { controller.resume(); _unityWidgetController = controller; } From 5c3781a3cfe9fb1aa58d4d0e1498a85aefb48dd9 Mon Sep 17 00:00:00 2001 From: timbotimbo Date: Sat, 2 Aug 2025 18:47:55 +0200 Subject: [PATCH 3/7] Add example.md for the example tab on pub.dev. --- README.md | 146 +++++++++++++++++++-------------------------- example/example.md | 117 ++++++++++++++++++++++++++++++++++++ 2 files changed, 180 insertions(+), 83 deletions(-) create mode 100644 example/example.md diff --git a/README.md b/README.md index bed4cd007..bfac9dd40 100644 --- a/README.md +++ b/README.md @@ -608,34 +608,23 @@ class UnityDemoScreen extends StatefulWidget { } class _UnityDemoScreenState extends State { - static final GlobalKey _scaffoldKey = - GlobalKey(); + UnityWidgetController? _unityWidgetController; @override Widget build(BuildContext context) { return Scaffold( - key: _scaffoldKey, - body: SafeArea( - bottom: false, - child: WillPopScope( - onWillPop: () async { - // Pop the category page if Android back button is pressed. - return true; - }, - child: Container( - color: Colors.yellow, - child: UnityWidget( - onUnityCreated: onUnityCreated, - ), - ), + body: Container( + color: Colors.yellow, + child: UnityWidget( + onUnityCreated: onUnityCreated, ), ), ); } // Callback that connects the created controller to the unity controller - void onUnityCreated(controller) { + void onUnityCreated(UnityWidgetController controller) { _unityWidgetController = controller; } } @@ -649,79 +638,71 @@ class _UnityDemoScreenState extends State { import 'package:flutter/material.dart'; import 'package:flutter_unity_widget/flutter_unity_widget.dart'; -void main() => runApp(const MyApp()); +void main() { + runApp( + const MaterialApp( + home: UnityDemoScreen(), + ), + ); +} -class MyApp extends StatefulWidget { - const MyApp({Key? key}) : super(key: key); +class UnityDemoScreen extends StatefulWidget { + const UnityDemoScreen({Key? key}) : super(key: key); @override - State createState() => _MyAppState(); + State createState() => _UnityDemoScreenState(); } -class _MyAppState extends State { - static final GlobalKey _scaffoldKey = - GlobalKey(); +class _UnityDemoScreenState extends State { UnityWidgetController? _unityWidgetController; double _sliderValue = 0.0; - @override - void initState() { - super.initState(); - } - @override Widget build(BuildContext context) { - return MaterialApp( - home: Scaffold( - key: _scaffoldKey, - appBar: AppBar( - title: const Text('Unity Flutter Demo'), - ), - body: Card( - margin: const EdgeInsets.all(8), - clipBehavior: Clip.antiAlias, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20.0), + return Scaffold( + appBar: AppBar( + title: const Text('Unity Flutter Demo'), + ), + body: Stack( + children: [ + UnityWidget( + onUnityCreated: onUnityCreated, + onUnityMessage: onUnityMessage, + onUnitySceneLoaded: onUnitySceneLoaded, ), - child: Stack( - children: [ - UnityWidget( - onUnityCreated: onUnityCreated, - onUnityMessage: onUnityMessage, - onUnitySceneLoaded: onUnitySceneLoaded, - fullscreen: false, - ), - Positioned( - bottom: 20, - left: 20, - right: 20, - // - child: Card( - elevation: 10, - child: Column( - children: [ - const Padding( - padding: EdgeInsets.only(top: 20), - child: Text("Rotation speed:"), - ), - Slider( - onChanged: (value) { - setState(() { - _sliderValue = value; - }); - setRotationSpeed(value.toString()); - }, - value: _sliderValue, - min: 0, - max: 20, - ), - ], - ), + + // Flutter UI Stacked on top of Unity to demo Flutter -> Unity interactions. + // On web this requires a PointerInterceptor widget. + Positioned( + bottom: 0, + // + child: SafeArea( + child: Card( + elevation: 10, + child: Column( + children: [ + const Padding( + padding: EdgeInsets.only(top: 20), + child: Text("Rotation speed:"), + ), + Slider( + onChanged: (value) { + setState(() { + _sliderValue = value; + }); + // Send value to Unity + setRotationSpeed(value.toString()); + }, + value: _sliderValue, + min: 0.0, + max: 1.0, + ), + ], ), ), - ], + ), ), - ), + ], ), ); } @@ -735,16 +716,16 @@ class _MyAppState extends State { ); } - // Communication from Unity to Flutter - void onUnityMessage(message) { - print('Received message from unity: ${message.toString()}'); - } - // Callback that connects the created controller to the unity controller - void onUnityCreated(controller) { + void onUnityCreated(UnityWidgetController controller) { _unityWidgetController = controller; } + // Communication from Unity to Flutter + void onUnityMessage(dynamic message) { + print('Received message from unity: ${message.toString()}'); + } + // Communication from Unity when new scene is loaded to Flutter void onUnitySceneLoaded(SceneLoaded? sceneInfo) { if (sceneInfo != null) { @@ -754,7 +735,6 @@ class _MyAppState extends State { } } } - ``` ## Props diff --git a/example/example.md b/example/example.md new file mode 100644 index 000000000..940256ffe --- /dev/null +++ b/example/example.md @@ -0,0 +1,117 @@ +# Example + + + +This example **requires** you to first follow the Readme setup and make an export in Unity. +An example Unity project can be found in `example/unity/DemoApp`. + +For Android and iOS we recommended to run this on a real device. Emulator support is very limited. + +## Flutter + + +```dart +import 'package:flutter/material.dart'; +import 'package:flutter_unity_widget/flutter_unity_widget.dart'; + +void main() { + runApp( + const MaterialApp( + home: UnityDemoScreen(), + ), + ); +} + +class UnityDemoScreen extends StatefulWidget { + const UnityDemoScreen({Key? key}) : super(key: key); + + @override + State createState() => _UnityDemoScreenState(); +} + +class _UnityDemoScreenState extends State { + UnityWidgetController? _unityWidgetController; + double _sliderValue = 0.0; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Unity Flutter Demo'), + ), + body: Stack( + children: [ + // This plugin's widget. + UnityWidget( + onUnityCreated: onUnityCreated, + onUnityMessage: onUnityMessage, + onUnitySceneLoaded: onUnitySceneLoaded, + ), + + // Flutter UI Stacked on top of Unity to demo Flutter -> Unity interactions. + // On web this requires a PointerInterceptor widget. + Positioned( + bottom: 0, + // + child: SafeArea( + child: Card( + elevation: 10, + child: Column( + children: [ + const Padding( + padding: EdgeInsets.only(top: 20), + child: Text("Rotation speed:"), + ), + Slider( + onChanged: (value) { + setState(() { + _sliderValue = value; + }); + // Send value to Unity + setRotationSpeed(value.toString()); + }, + value: _sliderValue, + min: 0.0, + max: 1.0, + ), + ], + ), + ), + ), + ), + ], + ), + ); + } + + // Callback that connects the created controller to the unity controller + void onUnityCreated(UnityWidgetController controller) { + _unityWidgetController = controller; + } + + // Communcation from Flutter to Unity + void setRotationSpeed(String speed) { + // Set the rotation speed of a cube in our example Unity project. + _unityWidgetController?.postMessage( + 'Cube', + 'SetRotationSpeed', + speed, + ); + } + + // Communication from Unity to Flutter + void onUnityMessage(dynamic message) { + print('Received message from unity: ${message.toString()}'); + } + + // Communication from Unity when new scene is loaded to Flutter + void onUnitySceneLoaded(SceneLoaded? sceneInfo) { + if (sceneInfo != null) { + print('Received scene loaded from unity: ${sceneInfo.name}'); + print( + 'Received scene loaded from unity buildIndex: ${sceneInfo.buildIndex}'); + } + } +} + +``` \ No newline at end of file From 1c8b6d8bbdaa027fd4018469fa18c413594afc1b Mon Sep 17 00:00:00 2001 From: timbotimbo Date: Sun, 3 Aug 2025 23:54:43 +0200 Subject: [PATCH 4/7] Handle icon and image in the readme that are broken on pub.dev. --- README.md | 43 ++++++++++++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index bfac9dd40..508e95db5 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ You will need to open and export a Unity project, even for running the example. ## Setup -In the tutorial below, there are steps specific to each platform, denoted by a :information_source: icon followed by +In the tutorial below, there are steps specific to each platform, denoted by a ℹ️ icon followed by the platform name (Android or iOS). You can click on its icon to expand it. ### Prerequisites @@ -142,7 +142,7 @@ After exporting Unity, you will need to make some small changes in your iOS or A You will likely need to do this **only once**. These changes remain on future Unity exports.
-:information_source: Android +ℹ️ Android 1. Setting the Android NDK @@ -323,7 +323,7 @@ allprojects {
- :information_source: iOS + ℹ️ iOS > Because of Apple's privacy manifest requirements, you need a minimal Unity version of 2021.3.35 or 2022.3.18 to publish an app. @@ -396,7 +396,7 @@ allprojects {
- :information_source: AR Foundation Android + ℹ️ AR Foundation Android 1. Check the version of the `XR Plugin Management` in the Unity package manager. Versions `4.3.1 - 4.3.3` contain a bug that breaks Android exports. Make sure to use a version <=`4.2.2` or >=`4.4`. @@ -425,7 +425,7 @@ allprojects {
- :information_source: AR Foundation iOS + ℹ️ AR Foundation iOS 1. Open the *ios/Runner/Info.plist* and add a camera usage description. For example: @@ -439,7 +439,7 @@ For example:
- :information_source: Vuforia Android + ℹ️ Vuforia Android 1. Your export should contain a Vuforia library in the `android/unityLibrary/libs/` folder. Currently named `VuforiaEngine.aar`. @@ -452,7 +452,7 @@ In case this gets outdated or broken, check the [Vuforia documentation](https://
- :information_source: Vuforia iOS + ℹ️ Vuforia iOS These steps are based on these [Vuforia docs](https://developer.vuforia.com/library/unity-extension/using-vuforia-engine-unity-library-uaal#ios-specific-steps) and [this comment](https://github.com/juicycleff/flutter-unity-view-widget/issues/314#issuecomment-785302253) @@ -486,7 +486,7 @@ We recommend using a physical iOS or Android device, as emulator support is limi Below are the limited options to use an emulator.
- iOS Simulators +ℹ️ iOS Simulators The `Target SDK` option in the Unity player settings is important here. - `Device SDK` exports an ARM build. (Which does **NOT** work on ARM simulators) @@ -530,7 +530,7 @@ The rest depends on the type of processor in your mac:
- Android emulators +ℹ️ Android emulators Unity only supports ARM build targets for Android. However most Android emulators are x86 which means they simply won't work. @@ -570,12 +570,18 @@ If you computer does not have an ARM processor, like most computers running on I 2. Use the method `postMessage` to send a string, using the GameObject name and the name of a behaviour method that should be called. +```dart +// Snippet of postMessage usage in the example project. +_unityWidgetController?.postMessage( + 'Cube', // GameObject name + 'SetRotationSpeed', // Function name in attached C# script + speed, // Function parameter (string) +); +``` ### Unity-Flutter 1. Select the GameObject that should execute the communication and go to **Inspector > Add Component > Unity Message Manager**. - - 2. Create a new `MonoBehaviour` subclass and add to the same GameObject as a script. 3. On this new behaviour, call `GetComponent()` to get a `UnityMessageManager`. @@ -583,6 +589,21 @@ If you computer does not have an ARM processor, like most computers running on I 4. Use the method `SendMessageToFlutter` to send a string. Receive this message using the `onUnityMessage` callback of a `UnityWidget`. +```C# +// Send a basic string to Flutter +SendMessageToFlutter("Hello there!"); +``` +```C# +// If you want to send multiple parameters or objects, use a JSON string. +// This is a random object serialized to JSON using Json.net. +JObject o = JObject.FromObject(new +{ + id = 1, + name = "Object 1", + whatever = 12 +}); +SendMessageToFlutter(o.ToString()); +``` ## Examples From 5fb0c575fdb7bbe5d1dca8c81a3a7574081e58b6 Mon Sep 17 00:00:00 2001 From: timbotimbo Date: Mon, 4 Aug 2025 00:53:16 +0200 Subject: [PATCH 5/7] Update flutter_lints and fix lint warnings. --- analysis_options.yaml | 3 +- .../lib/screens/no_interaction_screen.dart | 2 + example/lib/screens/orientation_screen.dart | 2 + example/lib/screens/simple_screen.dart | 2 + example/test/widget_test.dart | 2 +- lib/src/facade_widget.dart | 4 +- lib/src/io/device_method.dart | 4 +- lib/src/io/unity_widget.dart | 6 +-- lib/src/io/windows_unity_widget_view.dart | 2 +- lib/src/web/unity_widget.dart | 6 +-- lib/src/web/web_unity_widget_controller.dart | 37 +++++++++++++++---- pubspec.yaml | 2 +- 12 files changed, 50 insertions(+), 22 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index 655cb67bd..448d91f71 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1 +1,2 @@ -analyzer: \ No newline at end of file +include: package:flutter_lints/flutter.yaml + diff --git a/example/lib/screens/no_interaction_screen.dart b/example/lib/screens/no_interaction_screen.dart index 42881a6d3..4e6674239 100644 --- a/example/lib/screens/no_interaction_screen.dart +++ b/example/lib/screens/no_interaction_screen.dart @@ -1,3 +1,5 @@ +// ignore_for_file: avoid_print + import 'package:flutter/material.dart'; import 'package:flutter_unity_widget/flutter_unity_widget.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; diff --git a/example/lib/screens/orientation_screen.dart b/example/lib/screens/orientation_screen.dart index 0bee44516..4f4194b0c 100644 --- a/example/lib/screens/orientation_screen.dart +++ b/example/lib/screens/orientation_screen.dart @@ -1,3 +1,5 @@ +// ignore_for_file: avoid_print + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_unity_widget/flutter_unity_widget.dart'; diff --git a/example/lib/screens/simple_screen.dart b/example/lib/screens/simple_screen.dart index 9965a37f5..d853201e6 100644 --- a/example/lib/screens/simple_screen.dart +++ b/example/lib/screens/simple_screen.dart @@ -1,3 +1,5 @@ +// ignore_for_file: avoid_print + import 'package:flutter/material.dart'; import 'package:flutter_unity_widget/flutter_unity_widget.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart index 96b8ee3fd..ae6b56fab 100644 --- a/example/test/widget_test.dart +++ b/example/test/widget_test.dart @@ -13,7 +13,7 @@ import 'package:flutter_unity_widget_example/main.dart'; void main() { testWidgets('Verify Platform version', (WidgetTester tester) async { // Build our app and trigger a frame. - await tester.pumpWidget(MyApp()); + await tester.pumpWidget(const MyApp()); // Verify that platform version is retrieved. expect( diff --git a/lib/src/facade_widget.dart b/lib/src/facade_widget.dart index f83118fcf..ca38d911d 100755 --- a/lib/src/facade_widget.dart +++ b/lib/src/facade_widget.dart @@ -6,8 +6,8 @@ import 'package:flutter_unity_widget/src/facade_controller.dart'; import 'helpers/misc.dart'; class UnityWidget extends StatefulWidget { - UnityWidget({ - Key? key, + const UnityWidget({ + super.key, required this.onUnityCreated, this.onUnityMessage, this.fullscreen = false, diff --git a/lib/src/io/device_method.dart b/lib/src/io/device_method.dart index 063ed7623..b831f9543 100644 --- a/lib/src/io/device_method.dart +++ b/lib/src/io/device_method.dart @@ -159,14 +159,14 @@ class MethodChannelUnityWidget extends UnityWidgetPlatform { String? unitySrcUrl, }) { // ignore: no_leading_underscores_for_local_identifiers - final String _viewType = 'plugin.xraph.com/unity_view'; + const String _viewType = 'plugin.xraph.com/unity_view'; if (useAndroidViewSurf != null) useAndroidViewSurface = useAndroidViewSurf; final Map creationParams = unityOptions; if (defaultTargetPlatform == TargetPlatform.windows) { - return WindowsUnityWidgetView(); + return const WindowsUnityWidgetView(); } if (defaultTargetPlatform == TargetPlatform.android) { diff --git a/lib/src/io/unity_widget.dart b/lib/src/io/unity_widget.dart index 69ed20caf..7e490dbab 100644 --- a/lib/src/io/unity_widget.dart +++ b/lib/src/io/unity_widget.dart @@ -51,8 +51,8 @@ class AndroidUnityWidgetFlutter { typedef MobileUnityWidgetState = _UnityWidgetState; class UnityWidget extends StatefulWidget { - UnityWidget({ - Key? key, + const UnityWidget({ + super.key, required this.onUnityCreated, this.onUnityMessage, this.fullscreen = false, @@ -171,7 +171,7 @@ class _UnityWidgetState extends State { if (widget.enablePlaceholder) { return widget.placeholder ?? - Text('Placeholder mode enabled, no native code will be called'); + const Text('Placeholder mode enabled, no native code will be called'); } return UnityWidgetPlatform.instance.buildViewWithTextDirection( diff --git a/lib/src/io/windows_unity_widget_view.dart b/lib/src/io/windows_unity_widget_view.dart index 49f518d40..9308225a0 100644 --- a/lib/src/io/windows_unity_widget_view.dart +++ b/lib/src/io/windows_unity_widget_view.dart @@ -11,7 +11,7 @@ class _WindowsUnityWidgetViewState extends State { @override Widget build(BuildContext context) { // TODO: Rex Update windows view - return MouseRegion( + return const MouseRegion( child: Texture( textureId: 0, ), diff --git a/lib/src/web/unity_widget.dart b/lib/src/web/unity_widget.dart index 379124706..d56cb7554 100755 --- a/lib/src/web/unity_widget.dart +++ b/lib/src/web/unity_widget.dart @@ -10,8 +10,8 @@ import 'web_unity_widget_controller.dart'; import 'web_unity_widget_view.dart'; class UnityWidget extends StatefulWidget { - UnityWidget({ - Key? key, + const UnityWidget({ + super.key, required this.onUnityCreated, this.onUnityMessage, this.fullscreen = false, @@ -108,7 +108,7 @@ class _UnityWidgetState extends State { if (widget.enablePlaceholder) { return widget.placeholder ?? - Text('Placeholder mode enabled, no native code will be called'); + const Text('Placeholder mode enabled, no native code will be called'); } return WebUnityWidgetView( diff --git a/lib/src/web/web_unity_widget_controller.dart b/lib/src/web/web_unity_widget_controller.dart index cf4586d8a..fd0dbe5c9 100644 --- a/lib/src/web/web_unity_widget_controller.dart +++ b/lib/src/web/web_unity_widget_controller.dart @@ -104,22 +104,23 @@ class WebUnityWidgetController extends UnityWidgetController { void _registerEvents() { if (kIsWeb) { _messageListener = ((web.Event event) { - if (event is web.MessageEvent) { - final jsData = event.data; + final JSAny? jsEvent = event.jsify(); + if (_isJsObjectOfType(jsEvent, 'MessageEvent')) { + final jsData = (event as web.MessageEvent).data; String data = ""; // Handle a raw JS Object [Object object] instead of a json string. - if (jsData is JSObject) { + if (_isJsObject(jsData)) { try { - data = jsonStringify(jsData).toDart; + data = jsonStringify(jsData as JSObject).toDart; } catch (e) { log('Failed to stringify JS object', error: e); return; } } // this can be either a raw string like "unityReady" or a json string "{\"name\":\"\", ..}" - else if (jsData is JSString) { - data = jsData.toDart; + else if (_isJsString(jsData)) { + data = (jsData as JSString).toDart; } if (data.isNotEmpty) { @@ -255,8 +256,8 @@ class WebUnityWidgetController extends UnityWidgetController { .querySelector('flt-platform-view') ?.querySelector('iframe'); - if (frame != null && frame is web.HTMLIFrameElement) { - frame.focus(); + if (frame != null && _isJsObjectOfType(frame, 'HTMLIFrameElement')) { + (frame as web.HTMLIFrameElement).focus(); } } @@ -329,4 +330,24 @@ class WebUnityWidgetController extends UnityWidgetController { web.window.removeEventListener('message', _messageListener); } } + + // Manual type checks because `a is b` gives warning on js_interop types. + // https://dart.dev/tools/linter-rules/invalid_runtime_check_with_js_interop_types + + // TODO: Replace these `_isJs()` checks with `a.isA()` when upgrading to Dart >= 3.4. + + // This check could be replaced with `.isA()` on Dart 3.4+ + bool _isJsObject(JSAny? data) { + return data?.typeofEquals('object') ?? false; + } + + // This check could be simplified with `.isA()` on Dart 3.4+ + bool _isJsString(JSAny? data) { + return data?.typeofEquals('string') ?? false; + } + + // This check could be simplified with `.isA()` on Dart 3.4+ + bool _isJsObjectOfType(JSAny? data, String type) { + return _isJsObject(data) && (data as JSObject).instanceOfString(type); + } } diff --git a/pubspec.yaml b/pubspec.yaml index 803194d1f..302dc34f3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,7 +27,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^2.0.1 + flutter_lints: ^5.0.0 # Requires Flutter 3.24+ for development, to match github workflow. # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec From 834b1c9b9f23a6a8b47a0667bb368d6a2ab8455f Mon Sep 17 00:00:00 2001 From: timbotimbo Date: Mon, 4 Aug 2025 00:56:51 +0200 Subject: [PATCH 6/7] Update package version. --- CHANGELOG.md | 9 +++++++++ README.md | 2 +- pubspec.yaml | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b0138d02..09da4728d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## 2022.3.0 (master branch only) +* [Web] Switch to `package:web` to support WebAssembly (WASM). +* Updates minimum supported SDK version to Flutter 3.16/Dart 3.2. +* Fix lint warnings to improve static analysis. +* Small ReadMe fixes. + +**Note:** +> This release is unrelated to an old version `2022.3.0-alpha1`, which was based on the `feat/global_unity_controller` git branch. + ## 2022.2.2 (master branch only) * [Android] Fix touch detection when using Unity's New Input System. [#938](https://github.com/juicycleff/flutter-unity-view-widget/pull/938) * [Android] Workaround for mUnityplayer error in Unity plugins using the AndroidJavaProxy. [#908](https://github.com/juicycleff/flutter-unity-view-widget/pull/908) diff --git a/README.md b/README.md index 508e95db5..1f7dc5c10 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ is not compatible with other versions, it just mean it's been tested to work wit ## Installation -This plugin requires Flutter >= 3.3.0 +This plugin requires Flutter >= 3.16.0. First depend on the library by adding this to your packages `pubspec.yaml`: ```yaml diff --git a/pubspec.yaml b/pubspec.yaml index 302dc34f3..b31ee9311 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_unity_widget description: Flutter Unity 3D widget for embedding Unity game scenes in flutter. This library now supports Unity as a Library. -version: 2022.2.2 +version: 2022.3.0 #authors: # - Rex Raphael # - Thomas Stockx From a95eb1f5d677c35a4b8bb5714aef3d232be77238 Mon Sep 17 00:00:00 2001 From: Tim H Date: Mon, 4 Aug 2025 11:40:53 +0200 Subject: [PATCH 7/7] Document required Unity versions for publishing. --- README.md | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1f7dc5c10..92cb25df6 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,23 @@ the platform name (Android or iOS). You can click on its icon to expand it. - A `fuw-XXXX.unitypackage` file, found in the [*unitypackages*](https://github.com/juicycleff/flutter-unity-view-widget/tree/master/unitypackages) folder. Try to use the most recent unitypackage available. +### Unity versions for publishing +If you want to publish your app for Android or iOS, you need to satisfy certain Unity version requirements. + +**iOS** +Apple's [privacy manifest requirements](https://discussions.unity.com/t/apple-privacy-manifest-updates-for-unity-engine/936052) need a minimal Unity version of: +* 2021.3.35+ +* 2022.3.18+ +* 6000.0.0+ + +**Android** +> Starting November 1st, 2025, all new apps and updates to existing apps submitted to Google Play and targeting Android 15+ devices must support 16 KB page sizes. + +This requires [Unity versions](https://discussions.unity.com/t/info-unity-engine-support-for-16-kb-memory-page-sizes-android-15/1589588): +* 2021.3.48+ (Enterprise and Industry only) +* 2022.3.56+ +* 6000.0.38+ + ### Unity project setup These instructions assume you are using a new Unity project. If you open the example project from this repository, you can move on to the next section **Unity Exporting**. @@ -325,9 +342,6 @@ allprojects {
ℹ️ iOS - > Because of Apple's privacy manifest requirements, you need a minimal Unity version of 2021.3.35 or 2022.3.18 to publish an app. - - 1. Open the *ios/Runner.xcworkspace* (workspace, not the project) file in Xcode, right-click on the Navigator (not on an item), go to **Add Files to "Runner"** and add the *ios/UnityLibrary/Unity-Iphone.xcodeproj* file.