React-Native iOS - How can I navigate to a non-React-Native view (native iOS view controller) from a React-Native view with a button press?

I was able to figure this out. In my case, I am using an Obj-C base project (which is the RN default) with my own Swift native view controller. My solution is here in case this comes up for anyone else:

Simply put, the answer is to use an RCTBridge module to allow the RN JavaScript to call a native iOS method.

Here is an outline of the components, followed by the implementation:

  1. AppDelegate.h/.m - Initialize the RN JavaScript index file for the initial RN view, also setup a method to swap the root view controller to a native view controller (this method will be called from the RTCBridge module.

  2. MyViewController.swift - A normal UIViewController with a standard implementation.

  3. MyProject-Bridging-Header.h - provides Obj-C <-> Swift communication

  4. ChangeViewBridge.h/.m - This provides the binding to allow you to call native iOS methods from the RN JavaScript.

  5. index.ios.js - Initialize your custom RCTBridge module and call the bound method to switch to your native view with a button press.

AppDelegate.h:

#import <UIKit/UIKit.h>

@interface AppDelegate : UIResponder <UIApplicationDelegate> {
  NSDictionary *options;
  UIViewController *viewController;
}

@property (nonatomic, strong) UIWindow *window;

- (void) setInitialViewController;
- (void) goToRegisterView; // called from the RCTBridge module

@end

AppDelegate.m:

#import "AppDelegate.h"
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
#import "FidoTestProject-Swift.h" // Xcode generated import to reference MyViewController.swift from Obj-C

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  options = launchOptions;
  [self setInitialViewController];
  return YES;
}

- (void) setInitialViewController {
  NSURL *jsCodeLocation;

  jsCodeLocation = [NSURL URLWithString:@"http://192.168.208.152:8081/index.ios.bundle?platform=ios&dev=true"];

  RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation moduleName:@"FidoTestProject" initialProperties:nil launchOptions:options];

  self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
  UIViewController *rootViewController = [UIViewController new];
  rootViewController.view = rootView;
  self.window.rootViewController = rootViewController;

  viewController = rootViewController;

  [self.window makeKeyAndVisible];
}

// this method will be called from the RCTBridge
- (void) goToNativeView {
  NSLog(@"RN binding - Native View - MyViewController.swift - Load From "main" storyboard);
  UIViewController *vc = [UIStoryboard storyboardWithName:@"main" bundle:nil].instantiateInitialViewController;
  self.window.rootViewController = vc;
}

@end

MyViewController.swift:

class RegisterViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        print("MyViewController loaded...")
        // standard view controller will load from RN
    }
}

MyProject-Bridging-Header.h:

@import Foundation;
@import UIKit;
@import CoreLocation;
@import AVFoundation;

#import "React/RCTBridge.h"
#import "React/RCTBridgeModule.h"
#import "React/RCTBundleURLProvider.h"
#import "React/RCTRootView.h"
#import "AppDelegate.h"

ChangeViewBridge.h:

#import <React/RCTBridgeModule.h>

@interface ChangeViewBridge : NSObject <RCTBridgeModule>

- (void) changeToNativeView;

@end

ChangeViewBridge.m:

#import "RegisterBridge.h"
#import "FidoTestProject-Swift.h"
#import "AppDelegate.h"

@implementation ChangeViewBridge

// reference "ChangeViewBridge" module in index.ios.js
RCT_EXPORT_MODULE(ChangeViewBridge);

RCT_EXPORT_METHOD(changeToNativeView) {
  NSLog(@"RN binding - Native View - Loading MyViewController.swift");
  AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
  [appDelegate goToNativeView];
}

@end

index.ios.js

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 * @flow
 */

'use strict';

import React, { Component } from 'react';
import {
  AppRegistry,
  StyleSheet,
  Alert,
  Text,
  View,
  NativeModules,
  TouchableHighlight
} from 'react-native';

export default class FidoTestProject extends Component {

  constructor(props) {
     super(props)
     this.done = false;
   }

    _changeView() {
      this.done = true;
      this.render();
      NativeModules.ChangeViewBridge.changeToNativeView();
    }

  render() {
    if (!this.done) {
      return (
        <View style={styles.container}>
          <TouchableHighlight onPress={() => this._changeView()}>
            <Text color="#336699">
              Press to Change to Native View
            </Text>
          </TouchableHighlight>
        </View>
      );
    } else {
      return (<View></View>);
    }
  }
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  }
});

AppRegistry.registerComponent('FidoTestProject', () => FidoTestProject);

An update to this answer with Swift 5. Thanks to

https://github.com/facebook/react-native/issues/1148#issuecomment-102008892

https://stackoverflow.com/a/46007680/7325179 - answer by MStrapko

https://codersera.com/blog/react-native-bridge-for-ios/?unapproved=2851&moderation-hash=77e42524b246d2fda0f763a496156db5#comment-2851 - an elaborate explanation and tutorial by William Dawson

Getting into the Solution:

In AppDelegate.swift

import Foundation
import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
  var window: UIWindow?
  var bridge: RCTBridge!
  
  func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    let jsCodeLocation: URL
    
    jsCodeLocation = RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index.js", fallbackResource:nil)
    let rootView = RCTRootView(bundleURL: jsCodeLocation, moduleName: "RNModuleName", initialProperties: nil, launchOptions: launchOptions)
    
    self.window = UIWindow(frame: UIScreen.main.bounds)
    let reactNativeViewController = UIViewController()
    reactNativeViewController.view = rootView
    let reactNavigationController = UINavigationController(rootViewController: reactNativeViewController)
    self.window?.rootViewController = reactNavigationController
    self.window?.makeKeyAndVisible()
    
    return true
  }
//  func goToReactNative() {
//    window?.rootViewController?.dismiss(animated: true)
//  }
  func goNativeStoryboard() {
    DispatchQueue.main.async {
      let vc = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController()
      if let vc = vc {
        (self.window?.rootViewController as? UINavigationController)?.pushViewController(vc, animated: true)
      }
    }
  }
}

YourViewController.swift

Your regular code

YourApp-Bridging-Header. Please note there are some extra headers as well, that you might not need.

#import "React/RCTBridgeModule.h"
#import "React/RCTBridge.h"
#import "React/RCTEventDispatcher.h"
#import "React/RCTRootView.h"
#import "React/RCTUtils.h"
#import "React/RCTConvert.h"
#import "React/RCTBundleURLProvider.h"
#import "RCTViewManager.h"
#import "React/RCTEventEmitter.h"

ConnectingFile.swift

@objc(Connect)
class Connect: NSObject {
  @objc func goToNative() -> Void {
    DispatchQueue.main.async {
      if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
        appDelegate.goNativeStoryboard()
      }
    }
  }
}

Connect.m

#import "React/RCTViewManager.h"
@interface RCT_EXTERN_MODULE(Connect, RCTViewManager)
RCT_EXTERN_METHOD(goToNative)

@end

ReactNativeFile.js

import React, { Component } from 'react';
import { StyleSheet, View, NativeModules, Text, TouchableOpacity } from 'react-native';
const { Connect } = NativeModules;
export default class Feed extends Component {
  constructor(props) {
    super(props)
    this.done = false;
  }
  _changeView() {
    this.done = true;
    Connect.goToNative()
  }
  render() {
    return (
      <View style={styles.container}>
        <TouchableOpacity onPress={() => this._changeView()}>
          <Text color="#336699">
            Press to Change to Native View
            </Text>
        </TouchableOpacity>
      </View>
    );
  }
}
const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: 'pink',
    alignItems: 'center',
    justifyContent: 'center',
  }
});

That was it, it worked for me, hope it works for you as well. Thanks again to all the sources of references.


There is little improvement on this solution.With present solution there is no way to come back to React-Native from iOS. 

If you want to come back again from iOS to React-Native.Do the below

 // AppDelegate.h

    - (void) goToNativeView {
     UIViewController *vc =  [InitialViewController new];// This is your native iOS VC
     UINavigationController* navigationController = [[UINavigationController alloc] initWithRootViewController:vc];

      dispatch_async(dispatch_get_main_queue(), ^{
       // Never do the below, it will be difficult to come back to react-native

       // self.window.rootViewController = navigationController;

        // Do this instead
        [self.window.rootViewController presentViewController:navigationController animated:true completion:NULL];
      });
    }

//InitialViewController.m

    **-Create a button for go back to React-native and on button action dismiss this view controller like below.**

    // Dismiss the VC so controll go back from iOS to react-native
        [self dismissViewControllerAnimated:TRUE completion:nil];