Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Integrating React Native into Xamarin Project

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

like image 849
mylogon Avatar asked Dec 08 '16 17:12

mylogon


People also ask

Is React Native better than Xamarin?

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.

Is Xamarin still popular?

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.

Is Xamarin good for mobile development?

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.


Video Answer


1 Answers

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.');
    
like image 89
Kenny McClive Avatar answered Oct 02 '22 11:10

Kenny McClive