17

The RN doco and other examples show how to launch a React-Native view from a native iOS view controller, but not the other way around. Can someone explain how I can do this?

MStrapko
  • 511
  • 2
  • 6
  • 18

3 Answers3

23

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);
MStrapko
  • 511
  • 2
  • 6
  • 18
  • IXUAFDelegate from where we find this delegate? – seggy Jul 11 '19 at 14:21
  • 1
    Thanks for pointing that out @seggy. This is a custom delegate I missed when I was copy/pasting my example code out of my working project. I have edited the post to remove it from the example. – MStrapko Aug 21 '19 at 14:43
  • See the updated answer from Simeryn Denis below: https://stackoverflow.com/a/63372245/5474176 – MStrapko Aug 12 '20 at 14:24
  • The default for RN modules is a static library target, so I don't understand how you can use an AppDelegate and Storyboard?? wouldn't that require an Application target? when I tried this I got an error that "AppDelegate is not in scope". – mutable2112 Nov 02 '20 at 10:08
  • I'm not getting the native VC and there's no error as well...Could you please help me on this – Ephrim Daniel Feb 10 '21 at 05:31
  • I know its been a long time, but is the `-void (goToRegisterView)` in `AppDelegate.h` meant to be `-void (goToNativeView)` instead? – Jarrett Feb 28 '21 at 03:11
  • How is this saying going to this particular view controller (MyViewController)? – Neo Aug 11 '21 at 22:15
7

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.

Simeryn Denis
  • 168
  • 1
  • 10
6

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:

    // Dismiss the VC so controll go back from iOS to react-native
        [self dismissViewControllerAnimated:TRUE completion:nil];
Adriaan
  • 17,741
  • 7
  • 42
  • 75
Chandramani
  • 871
  • 1
  • 12
  • 11
  • From my original question: "The RN doco and other examples show how to launch a React-Native view from a native iOS view controller, but not the other way around." There are many available examples of how to go from native to RN. This question and answer were to show how to go from RN to native. – MStrapko Aug 22 '19 at 21:04