7

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 enter image description here
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.

enter image description here
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

mylogon
  • 2,772
  • 2
  • 28
  • 42
  • 1
    Do you advance more in this area? I'm in the same boat. I wanna use React or NativeScript for UI only and F# for logic. – mamcx Jul 20 '17 at 20:40
  • 1
    @mamcx Ah. I thought I'd gotten back to you on this - must have gotten distracted. Basically, no. It kind of fizzled out and I stopped burning all my time on it. We've switched platform since, so it's not really crossed my mind since - sorry :/ – mylogon Aug 11 '17 at 11:34

2 Answers2

11

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

  1. Create a Cocoa Touch Static Library project in Xcode.
  2. Install React Native in the same directory.

    npm install react-native
    
  3. 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.
  4. Add React to the Target Dependencies build phase. (Screenshot)
  5. Include all the React targets in the Link Binary With Libraries build phase. (Screenshot)
  6. Be sure to include -lc++ in the Other Linker Flags build setting.
  7. Use lipo to create a universal library (fat file). See Building Universal Native Libraries section in Xamarin documentation.

Create a Xamarin App

  1. Create a new iOS Single View App project/solution in Visual Studio. (Screenshot)
  2. Add an iOS Bindings Library project to the solution. (Screenshot)
  3. Add your universal static library as a Native Reference to the bindings library project.
  4. 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)
  5. 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
    {
    
    }
    
  6. 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);
    }
    
  7. Add a reference to the bindings library project in the app project.
  8. 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();
    
  9. 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

  1. Add a class to your iOS app project.
  2. Have the class inherit RCTBridgeModule (from your bindings library).

    public class TestClass : RCTBridgeModule
    
  3. 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";
    
  4. 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;
    
  5. 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);
    
  6. 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.
    • I'm not sure what isSync is.
  7. 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"));
    

Call Your Native Module from JavaScript

  1. Import NativeModules into your JavaScript file.

    import { NativeModules } from 'react-native';
    
  2. Call one of the methods you exported.

    NativeModules.TestClass.test('C# called successfully.');
    
Kenny McClive
  • 126
  • 2
  • 5
  • 1
    I've not even finished reading this and I'm already buzzing. 10/10 answer. In the end, we went with Unity, of all things, so this is no longer a professional issue. However, this has come just in time for my next personal project. Thanks for your time and effort – mylogon Dec 12 '17 at 23:15
  • @mylogon I realized that I missed a couple things so I updated my answer. – Kenny McClive Dec 15 '17 at 00:54
  • @KennyMcClive `npm init && npm install react-native --save` to save modules in the folder with static lib code – Mando Mar 20 '18 at 19:34
  • Here is a good guide from `facebook` on how to manually link everything: https://facebook.github.io/react-native/docs/linking-libraries-ios.html – Mando Mar 20 '18 at 19:46
  • @KennyMcClive I've created the xcode proj and linked all the RN projects, configured everything correct and able to do a clean build for the project. But when I'm using `xcodebuild` to create a static library - it fails because it's unable to find some .a files, e.g. error: `libtool: can't open file: .../ReactNativeStaticLib/node_modules/react-native/Libraries/LinkingIOS/build/Release-iphoneos/libRCTLinking.a (No such file or directory)`. The `libRCTLinking` can be built without an error and I can see .a file created (but in different folder). How can I handle it? – Mando Mar 20 '18 at 23:11
  • @AlexeyStrakh I think I had a similar problem. I never could figure out how to get it to build from the command-line. I used the Xcode GUI to build the various architectures and then used lipo to combine the binaries into a fat file. Let us know if you are able to resolve that. – Kenny McClive Mar 21 '18 at 21:10
  • @KennyMcClive this guy actually created the script to automate this fat lib creation from scratch: https://github.com/voydz/xamarin-react-native, I've used that and was able to generate required static fat lib and then wrapped that into xamarin binding project. And I'm still curious, how did you create it via Xcode GUI, can you please update your response with steps? – Mando Mar 28 '18 at 09:05
  • @AlexeyStrakh What exactly do you want to know? How to create the Xcode project or how to build the fat file? – Kenny McClive Mar 29 '18 at 14:39
  • @KennyMcClive I've created the Xcode project and configured as required but unable to generate `.a` file neither with UI (don't see that option) nor with `xcodebuild` (the error posted several comments above) – Mando Apr 03 '18 at 18:33
  • @AlexeyStrakh I created the .a file by essentially following the Xamarin documentation in step 7 of the "Build a Static Library" section, but using the Xcode GUI to perform the individual builds instead of executing xcodebuild. I first built it using "Generic iOS Device" as the destination. Then, I built it using one of the simulators as the destination. Finally, I copied the individual .a files to the same folder and ran lipo to combine them. You can right-click the product in the tree and select "Show in Finder" to find the outputs. I hope that helps. – Kenny McClive Apr 16 '18 at 22:04
1

Here are the fully automated scripts to create a fat static lib and Xamarin Binding project to wrap it into Xamarin Binding .dll:

https://github.com/voydz/xamarin-react-native

Mando
  • 11,414
  • 17
  • 86
  • 167