Integrate React Native into a Xamarin project
My task is to see if it is possible to integrate React Native into a Xamarin.Forms project.
I think I'm close to achieving this, but I can't say for sure. I know this is a weird/regressive solution, but I'd like to try it anyway and see if I can beat it...
Introduction
My employer wanted to see if it was possible to use React Native for UI and C# for business logic. People are exploring this as a solution so that UI/UX teams can use RN to do their work and we (the development team) can link logic to it.
Everything I've done so far has been done by
cd'ing the terminal into the project directory and running 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 selected the Xcode project that React Native outputs and started running . Then I made a change to the line that AppDelegate
was looking for the main.jsbundle file .
Then, I added a static library as the target of this project. Compare with the build phase of the app and then added all the same linked libraries. I have since created a Xamarin.Forms solution. As I only create iOS libraries, I create an iOS.Binding project. I added the Xcode .a lib as a native reference. In the file I have created the interface with the following codeApiDefinition.cs
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
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.
Build a Static Library
- Create a Cocoa Touch Static Library project in Xcode.
Install React Native in the same directory.
npm install react-native
- Add all the React Xcode projects to your project. (Screenshot) You can look at the .pbxproj file of an existing React Native app for clues on how to find all these.
- Add React to the Target Dependencies build phase. (Screenshot)
- Include all the React targets in the Link Binary With Libraries build phase. (Screenshot)
- Be sure to include
-lc++
in the Other Linker Flags build setting. - Use lipo to create a universal library (fat file). See Building Universal Native Libraries section in Xamarin documentation.
Create a Xamarin App
- Create a new iOS Single View App project/solution in Visual Studio. (Screenshot)
- Add an iOS Bindings Library project to the solution. (Screenshot)
- Add your universal static library as a Native Reference to the bindings library project.
- Set Frameworks to
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 forSystem
,Foundation
, andUIKit
.// @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 forSystem
,System.Runtime.InteropServices
, andFoundation
.[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 a reference to the bindings library project in the app project.
Add the following code to the
FinishedLaunching
method in AppDelegate.cs. Don't forget to add ausing
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.
Create a Native Module
- Add a class to your iOS app project.
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 returntrue
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 anIntPtr
instead of anRCTMethodInfo
. 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.- I'm not sure what
isSync
is.
Please 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"));
Calling native modules from JavaScript
Import
NativeModules
your JavaScript file.import { NativeModules } from 'react-native';
Call one of your exported methods.
NativeModules.TestClass.test('C# called successfully.');