I have been tasked to see if it is possible to integrate React Native into a Xamarin.Forms project.
I think I came fairly close to achieving this, but I can't say for sure. I'm aware that this is a bit of a weird/ backwards solution, but I'd like to have a go at it anyway to see if I can beat it...
Intro
My employer is wanting to see if it is possible to use React Native for UI and use C# for the business logic. It is being explored as a solution so that the UI/UX team can produce work with RN and we (the dev team) can link in the logic to it.
What I've tried so far
I took the Xcode project that React Native outputted and started by removing the dependancy of a local Node service by cd'ing terminal into the project directory and ran react-native bundle --entry-file index.ios.js --platform ios --dev false --bundle-output ios/main.jsbundle --assets-dest ios
(taken from this blog post). I then made the change to the AppDelegate
line where it's looking for the main.jsbundle file.
I then added a static library as a target for the project. Comparing with the app's build phases, I then added all the same link libraries
After this, I created a Xamarin.Forms solution. As I had only created the iOS library, I created a iOS.Binding project. I added the Xcode .a lib as a native reference. Within the ApiDefinition.cs
file I created the interface with the following code
BaseType(typeof(NSObject))]
interface TheViewController
{
[Export("setMainViewController:")]
void SetTheMainViewController(UIViewController viewController);
}
To which, in the Xcode project, created a TheViewController
class. The setMainViewController:
was implemented in the following way:
-(void)setMainViewController:(UIViewController *)viewController{
AppDelegate * ad = (AppDelegate*)[UIApplication sharedApplication].delegate;
NSURL * jsCodeLocation = [NSURL fileURLWithPath:[[NSBundle mainBundle]pathForResource:@"main" ofType:@"jsbundle"]];
RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
moduleName:@"prototyper"
initialProperties:nil
launchOptions:ad.savedLaunchOptions];
rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1];
ad.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
viewController.view = rootView;
ad.window.rootViewController = viewController;
[ad.window makeKeyAndVisible];
}
Where I am effectively trying to pass in a UIViewController
from Xamarin for the React Native stuff to add itself to.
I am calling this from Xamarin.iOS in the following way:
private Binding.TheViewController _theViewController;
public override void ViewDidLoad()
{
base.ViewDidLoad();
_theViewController = new TheViewController();
_theViewController.SetTheMainViewController(this);
}
This class is implementing PageRenderer
, overriding the Xamarin.Forms' ContentPage
using
[assembly:ExportRenderer(typeof(RNTest.MainViewController), typeof(RNTest.iOS.MainViewController))]
Well, after all of this, I went to go and deploy to device and, expectedly, hit by a number of errors. The AOT compiler is going into my lib and trying to do it's magic and throws a number of linker errors in the React Native projects, as shown below.
Pastebin dump of full Build Output
I was intending on setting up more methods in the binding to set callbacks etc to start building some functionality regarding passing information back and forth with the Objective-C, which I was going to pass into the React Native with some native code link.
Summary
I know it's pretty long breathed, but if we can get this off the ground, then we can basically do all of our (fairly comlex) business logic in C# and leave all the UI changes to the dedicated UI team, who have a strong preference for React Native (fair enough, with their prototype being in pretty good condition). Really, it's all just another POC that I've been putting together for the next major release of our app.
If anyone can think of a better way of doing this, I am all ears. I have, of course, glazed over some of the details, so if anything needs clarifying then please ask and I will ammend.
Many, many thanks.
Luke
Though both Xamarin and React Native offer near-native performance, Xamarin runs the fastest code on Android and iOS and has a user interface (UI) for using native tools. TLDR: In Xamarin vs. React Native, Xamarin has more brownie points for native-like performance. Xamarin wins.
Popularity. With over 1.6 million developers across 120 countries, Xamarin has developed quite the user base over the years. However, this is largely due to the fact that it is one of the oldest frameworks out there.
Since its appearance in 2011, Xamarin has become a great option for cross-platform app development, a faster way to build iOS, Android, and Windows apps.
I was able to get this working using the steps below. There's a lot here so please forgive me if I missed a detail.
Install React Native in the same directory.
npm install react-native
-lc++
in the Other Linker Flags build setting.JavaScriptCore
and Linker Flags to -lstdc++
in the properties for the native reference. This fixes the linker errors mentioned in the original question. Also enable Force Load. (Screenshot)Add the following code to ApiDefinition.cs. Be sure to include using
statements for System
, Foundation
, and UIKit
.
// @interface RCTBundleURLProvider : NSObject
[BaseType(typeof(NSObject))]
interface RCTBundleURLProvider
{
// +(instancetype)sharedSettings;
[Static]
[Export("sharedSettings")]
RCTBundleURLProvider SharedSettings();
// -(NSURL *)jsBundleURLForBundleRoot:(NSString *)bundleRoot fallbackResource:(NSString *)resourceName;
[Export("jsBundleURLForBundleRoot:fallbackResource:")]
NSUrl JsBundleURLForBundleRoot(string bundleRoot, [NullAllowed] string resourceName);
}
// @interface RCTRootView : UIView
[BaseType(typeof(UIView))]
interface RCTRootView
{
// -(instancetype)initWithBundleURL:(NSURL *)bundleURL moduleName:(NSString *)moduleName initialProperties:(NSDictionary *)initialProperties launchOptions:(NSDictionary *)launchOptions;
[Export("initWithBundleURL:moduleName:initialProperties:launchOptions:")]
IntPtr Constructor(NSUrl bundleURL, string moduleName, [NullAllowed] NSDictionary initialProperties, [NullAllowed] NSDictionary launchOptions);
}
// @protocol RCTBridgeModule <NSObject>
[Protocol, Model]
[BaseType(typeof(NSObject))]
interface RCTBridgeModule
{
}
Add the following code to Structs.cs. Be sure to include using
statements for System
, System.Runtime.InteropServices
, and Foundation
.
[StructLayout(LayoutKind.Sequential)]
public struct RCTMethodInfo
{
public string jsName;
public string objcName;
public bool isSync;
}
public static class CFunctions
{
[DllImport ("__Internal")]
public static extern void RCTRegisterModule(IntPtr module);
}
Add the following code to the FinishedLaunching
method in AppDelegate.cs. Don't forget to add a using
statement for the namespace of your bindings library and specify the name of your React Native app.
var jsCodeLocation = RCTBundleURLProvider.SharedSettings().JsBundleURLForBundleRoot("index", null);
var rootView = new RCTRootView(jsCodeLocation, "<Name of your React app>", null, launchOptions);
Window = new UIWindow(UIScreen.MainScreen.Bounds);
Window.RootViewController = new UIViewController() { View = rootView };
Window.MakeKeyAndVisible();
Add the following to Info.plist.
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>localhost</key>
<dict>
<key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
</dict>
</dict>
At this point, you should be able to run any React Native app by launching the React Packager (react-native start
) in the corresponding directory. The following sections will show you how to call C# from React Native.
Have the class inherit RCTBridgeModule
(from your bindings library).
public class TestClass : RCTBridgeModule
Add the ModuleName
method to your class. Change the value returned to whatever you want to call the class in JavaScript. You can specify empty string to use the original.
[Export("moduleName")]
public static string ModuleName() => "TestClass";
Add the RequiresMainQueueSetup
method to your class. I think this will need to return true
if you implement a native (UI) component.
[Export("requiresMainQueueSetup")]
public static bool RequiresMainQueueSetup() => false;
Write the method that you want to export (call from JavaScript). Here is an example.
[Export("test:")]
public void Test(string msg) => Debug.WriteLine(msg);
For each method that you export, write an additional method that returns information about it. The names of each of these methods will need to start with __rct_export__
. The rest of the name doesn't matter as long as it is unique. The only way I could find to get this to work was to return an IntPtr
instead of an RCTMethodInfo
. Below is an example.
[Export("__rct_export__test")]
public static IntPtr TestExport()
{
var temp = new RCTMethodInfo()
{
jsName = string.Empty,
objcName = "test: (NSString*) msg",
isSync = false
};
var ptr = Marshal.AllocHGlobal(Marshal.SizeOf(temp));
Marshal.StructureToPtr(temp, ptr, false);
return ptr;
}
jsName
is the name you want to call the method from JavaScript. You can specify empty string to use the original.objcName
is the equivalent Objective-C signature of the method.isSync
is.Register your class before launching the view in AppDelegate.cs. The name of the class will be the fully-qualified name with underscores instead of dots. Here is an example.
CFunctions.RCTRegisterModule(ObjCRuntime.Class.GetHandle("ReactTest_TestClass"));
Import NativeModules
into your JavaScript file.
import { NativeModules } from 'react-native';
Call one of the methods you exported.
NativeModules.TestClass.test('C# called successfully.');
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With