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:
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.MyViewController.swift
- A normalUIViewController
with a standard implementation.MyProject-Bridging-Header.h
- providesObj-C <->
Swift communicationChangeViewBridge.h
/.m
- This provides the binding to allow you to call native iOS methods from the RN JavaScript.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];