React Native Tutorial: Integrating in an Existing App

Christine Abernathy
react native tutorial

Adding React Native to an existing Swift app

The React Native framework lets you build native apps using React concepts. With React Native, you can build responsive native apps while enjoying the benefits of various web development paradigms, such as viewing your changes without recompiling the code.

Have you wanted to see how React Native fits into your app and development workflow, but without completely converting your existing app? Good news – you can incrementally add React Native to your app by inserting one or more React Native views into your app and letting React Native drive that experience.

This React Native tutorial will guide you through integrating React Native into an existing Swift application. You’ll build upon an existing application called Mixer that displays mixer events and gives you an opportunity to rate them.

Before you get started, be sure to check out the React Native Tutorial: Building Apps with JavaScript that walks you through the basics. You’ll also need some familiarity with CocoaPods to work through this tutorial.

Getting Started

Download the starter project for this tutorial and unzip it. Run the project found in the Mixer-Starter/ios folder to see what you have to work with.

The home screen displays a list of mixers. Tap the first one to see the mixer details, then tap Add Rating to see the view where you can add your own rating. It’s presently empty – and waiting for your upcoming implemention using React Native! :]

Starter app overview

Add a React Native View

Close the Mixer project in Xcode. Before you start coding, you’ll have to install the prerequisites for React Native.

If you don’t have CocoaPods installed, execute the following command in Terminal:

gem install cocoapods

Next, install Homebrew using the instructions on the Homebrew website.

Then, execute the following command in Terminal to install Node.js through Homebrew:

brew install node

Finally, use the following command to instruct Homebrew to install watchman, which is used by React Native to detect file changes:

brew install watchman

You now have the basic React Native scaffolding in place. Now you can install the modules your project will use.

In Terminal, navigate to the starter project’s js subdirectory and create a file named package.json. Add the following to the file:

{
  "name": "mixer",
  "version": "1.0.0",
  "private": true,
  "description": "Mixer",
  "scripts": {
    "start": "node node_modules/react-native/local-cli/cli.js start"
  },
  "dependencies": {
    "react": "~15.3.1",
    "react-native": "~0.34.0"
  }
}

This lists the dependencies for your app and sets up the start script. Run the following command to install the required Node.js modules:

npm install

You should see a new node_modules subdirectory that contains the React and React Native modules. The React Native module includes code needed to integrate with a native app. You’ll use CocoaPods to make Xcode aware of these libraries.

In Terminal, navigate to the starter project’s ios subdirectory and create a file named Podfile with the following content:

use_frameworks!
target 'Mixer'
pod 'React', :path => '../js/node_modules/react-native', :subspecs => [
  'Core',
  'RCTImage',
  'RCTNetwork',
  'RCTText',
  'RCTWebSocket',
]

The above configuration specifies a subset of libraries to load from the React Podspec. The specs listed here allow you to work with features such as views, text, and images.

Run the following command to install the dependencies:

pod install

Your output should look like this:

Analyzing dependencies
Fetching podspec for `React` from `../js/node_modules/react-native`
Downloading dependencies
Installing React (0.34.0)
Generating Pods project
Integrating client project
 
[!] Please close any current Xcode sessions and use `Mixer.xcworkspace` for this project from now on.
Pod installation complete! There are 5 dependencies from the Podfile and 1 total pod installed.

Open Mixer.xcworkspace, and run your app to verify you don’t have any build errors. The app should look exactly the same as it did before:

Mixer Home

Creating a Simple View

Using your favorite text editor, create a new file named index.ios.js and save it in the js directory. Add the following content to that file:

'use strict';
// 1
import React from 'react';
import ReactNative, {
  AppRegistry,
  StyleSheet,
  Text,
  View,
} from 'react-native';
 
// 2
const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: 'green',
  },
  welcome: {
    fontSize: 20,
    color: 'white',
  },
});
 
// 3
class AddRatingApp extends React.Component {
  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.welcome}>We're live from React Native!!!</Text>
      </View>
    )
  }
}
 
// 4
AppRegistry.registerComponent('AddRatingApp', () => AddRatingApp);

Here’s a step-by-step of what’s going on above:

  1. This loads the react and react-native modules, and the destructuring assignments let you drop the React/ReactNative prefixes when calling methods in those modules.
  2. Next, you define the stylesheets used in the UI.
  3. The next bit defines AddRatingApp with render() to display some welcome text.
  4. AddRatingApp serves as the application entry point’s root component.

In Xcode, open AddRatingViewController.swift and add the following import directive:

import React

Next, add the following instance variable that represents the React Native root view:

var addRatingView: RCTRootView!

Then, add the following code to the end of viewDidLoad():

addRatingView = RCTRootView(
    bundleURL: URL(string: "http://localhost:8081/index.ios.bundle?platform=ios"),
    moduleName: "AddRatingApp",
    initialProperties: nil,
    launchOptions: nil)
self.view.addSubview(addRatingView)

This initializes an instance of RCTRootView with the app bundle served up from index.ios.js. It configures AddRatingApp as module name to run initially.

Finally, set the root view’s frame in viewDidLayoutSubviews by adding the following code to the end:

addRatingView.frame = self.view.bounds

For security reasons, Apple by default blocks HTTP access to URLs. You’ll have to make an exception so you can load your bundle from the development server running on http://localhost.

In Xcode, open Info.plist and perform the following:

  1. Add the NSAppTransportSecurity key as a Dictionary type.
  2. Add the NSExceptionDomains key with Dictionary type under NSAppTransportSecurity.
  3. Add a key named localhost of type Dictionary under NSExceptionDomains.
  4. Add the NSTemporaryExceptionAllowsInsecureHTTPLoads key with the value YES under localhost.

When done, your Info.plist should look like the following:

Mixer Info.plist

In Terminal, go to your js directory and execute the following command to start the React Native development server:

npm start

After a few seconds, you should see something like the following:

Development server running

Note: If you receive an error stating that none of the files are listed in global config root_files then run the following command to initialize Git: git init.

If you still see the same error, run the following command to initialize a watchman configuration file in the js directory: echo "{}" > .watchmanconfig

Run the project in Xcode. Tap any mixer, tap Add Rating, and you should see the welcome text view:

Mixer first React Native app

The first time you run your app, it may take a little time for the packager to load the bundle.

Setting Up the View with an Existing Bridge

Your JavaScript and native code communicate back and forth in React Native via a brige created using the JavaScriptCore Framework.

mixer-theory-bridge-3

The bridge communication is batched and asynchronous so it remains performant and that message calls are serialized across the bridge. To see how this plays out, take a look at how you display the initial React Native view:

    1. Native: initializes the bridge.
    2. Sends a message through the bridge to the JavaScript code to run the application.
    1. JavaScript: runs the initial AddRatingApp component that was registered.
    2. Calls render for the component which displays a View and Text node.
    3. Batches and sends a message through the bridge to the Native code responsible for creating and displaying the views.

mixer-theory-bridge-msgs-1

The view layout is first computed using css-layout, then displayed using UIKit that translates View into UIView and Text into UILabel.

The following threads and queues manage the code execution to ensure responsiveness:

  • Main Thread: This thread handles the display of native views through UIKit.
  • Shadow Queue: This GCD queue computes the native view layout.
  • JavaScript Queue: This queue manages the execution of the JavaScript code.
  • Modules Queue: By default, each custom native module gets its own GCD queue. You’ll learn about native modules shortly.

In the previous section, you created a view by calling RCTRootView(_:moduleName:initialProperties:launchOptions). This is fine if you’re only going to have one RCTRootView in your app. But when working with multiple React Native views, it’s best to first create an RCTBridge instance that you can reuse to set up additional views.

Create an empty Swift file named MixerReactModule.swift and populate it with the following:

import Foundation
import React
 
class MixerReactModule: NSObject {  
  static let sharedInstance = MixerReactModule()
}

This uses the singleton pattern to lazily create a MixerReactModule instance when first accessed.

Add the following variable to the class:

var bridge: RCTBridge?

Then, add the required RCTBridgeDelegate delegate method sourceURL(for:) as an extension to the end of the file:

extension MixerReactModule: RCTBridgeDelegate {
  func sourceURL(for bridge: RCTBridge!) -> URL! {
    return URL(string: "http://localhost:8081/index.ios.bundle?platform=ios")
  }
}

Now, add the following methods to the class:

func createBridgeIfNeeded() -> RCTBridge {
  if bridge == nil {
    bridge = RCTBridge.init(delegate: self, launchOptions: nil)
  }
  return bridge!
}
 
func viewForModule(_ moduleName: String, initialProperties: [String : Any]?) -> RCTRootView {
  let viewBridge = createBridgeIfNeeded()
  let rootView: RCTRootView = RCTRootView(
    bridge: viewBridge,
    moduleName: moduleName,
    initialProperties: initialProperties)
  return rootView
}

viewForModule(_:initialProperties) calls createBridgeIfNeeded() to create an RCTBridge instance if one doesn’t exist. It then calls RCTRootView(_:moduleName:initialProperties) to create an RCTRootView instance with this bridge. Creating additional root views will reuse the existing bridge.

Now, open AddRatingViewController.swift and replace the addRatingView assignment in viewDidLoad() with the following:

addRatingView = MixerReactModule.sharedInstance.viewForModule(
  "AddRatingApp",
  initialProperties: nil)

Run your app; you should see no changes, but at this point you’ve set things up to let you easily add additional React Native views.

Mixer first React Native app

Adding a Navigator to your View

If you hadn’t noticed, your new view is a little green and in desperate need of some navigation.

Create a new file named AddRatingApp.js in your js directory. Add the following code to it:

'use strict';
 
import React from 'react';
import ReactNative, {
  StyleSheet,
  Text,
  View,
} from 'react-native';
 
const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: 'green',
  },
  welcome: {
    fontSize: 20,
    color: 'white',
  },
});
 
class AddRatingApp extends React.Component {
  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.welcome}>We're live from React Native!!!</Text>
      </View>
    )
  }
}
 
module.exports = AddRatingApp;

If you’re astute, you’ll notice that this looks suspiciously like the content in index.ios.js. Moving things around simply makes it easier to add the navigation.

Open index.ios.js and replace its content with the following:

'use strict';
 
import {AppRegistry} from 'react-native';
 
const AddRatingApp = require('./AddRatingApp');
 
AppRegistry.registerComponent('AddRatingApp', () => AddRatingApp);

The modified code now requires the AddRatingApp component to set up the initial view.

Hit Cmd+R in your simulator to reload the app. There should be no changes.

You’ll be using the Navigator component to set up your navigation bar. To set it up, you provide it with a renderScene function that renders your scene. You can set up your own navigation bar by passing in a navigationBar prop that consists
of a Navigator.NavigationBar component.

To customize Navigator.NavigationBar, you pass in a routeMapper prop that defines the left button, title, and right button.

Open AddRatingApp.js and add the following imports for Navigator and TouchableOpacity:

  ...
  Navigator,
  TouchableOpacity,
} from 'react-native';

You’ll use TouchableOpacity to implement touch handlers for the navigation item buttons.

Replace styles with the following:

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  content: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: 'green',
  },
  welcome: {
    fontSize: 20,
    color: 'white',
  },
  navBar: {
    backgroundColor: '#25507b',
  },
  navBarText: {
    fontSize: 16,
    marginVertical: 10,
  },
  navBarTitleText: {
    color: 'white',
    fontWeight: '500',
    marginVertical: 9,
  },
  navBarLeftButton: {
    paddingLeft: 10,
  },
  navBarRightButton: {
    paddingRight: 10,
  },
  navBarButtonText: {
    color: 'white',
  },
});

In just a moment, you’ll add private methods inside the AddRatingApp class to render the navigation items. But first, add the following method to render the navigation content:

_renderScene(route, navigator) {
  return (
    <View style={styles.content}>
      <Text style={styles.welcome}>We're live from React Native!!!</Text>
    </View>
  );
}

This displays the welcome text you know and love. Next, add the render method for the navigation bar’s title:

_renderNavTitle(route, navigator, index, navState) {
  return <Text style={styles.navBarTitleText}>{route.title}</Text>;
}

This simply returns the title property the route passed in.

Next, add the render method for the navigation bar’s left item:

_renderNavLeftItem(route, navigator, index, navState) {
  return (
    <TouchableOpacity
      onPress={() => console.log('Cancel button pressed')}
      style={styles.navBarLeftButton}>
      <Text style={[styles.navBarText, styles.navBarButtonText]}>
        Cancel
      </Text>
    </TouchableOpacity>
  );
}

This wraps a Text component inside a TouchableOpacity component so it can handle touch events. onPress of the touch handler logs the fact that the button was pressed.

Do the same for the navigation bar’s right item:

_renderNavRightItem(route, navigator, index, navState) {
  return (
    <TouchableOpacity
      onPress={() => console.log('Save button pressed')}
      style={styles.navBarRightButton}>
      <Text style={[styles.navBarText, styles.navBarButtonText]}>
        Save
      </Text>
    </TouchableOpacity>
  );
}

Finally, modify render as shown below to return a Navigator component:

render() {
  return (
    <Navigator
      debugOverlay={false}
      style={styles.container}
      initialRoute={{title: 'Add Rating'}}
      renderScene={this._renderScene.bind(this)}
      navigationBar={
        <Navigator.NavigationBar
          routeMapper={{
            LeftButton: this._renderNavLeftItem.bind(this),
            RightButton: this._renderNavRightItem.bind(this),
            Title: this._renderNavTitle.bind(this),
          }}
          style={styles.navBar}
        />
      }
    />
  );
}

The initialRoute prop sets up the properties for the initial scene – specifically, the title that’s used in _renderNavTitle.

Reload the app from the simulator. You should now see the navigator bar on top of the view:

Mixer add navigation

Now to test the navigation buttons. Tap the left one and Xcode should log something like the following:

2016-09-21 17:20:13.085 [info][tid:com.facebook.react.JavaScript] Cancel button pressed

Tapping the right one should log something like this:

2016-09-21 17:20:27.838 [info][tid:com.facebook.react.JavaScript] Save button pressed

Note: You can also view app logs by running the following command in Terminal: react-native log-ios.

Communicating With the React Native View

When you instantiate RCTRootView, you can pass data into the initialProperties parameter. This data is then passed on as initial props to the root component. You can also update the properties later on by passing new values to your RCTRootView instance’s appProperties property.

Open AddRatingViewController.swift and modify the addRatingView assignment to pass in the mixer identifier and current rating as follows:

addRatingView = MixerReactModule.sharedInstance.viewForModule(
      "AddRatingApp",
      initialProperties: ["identifier": mixer.identifier, "currentRating": currentRating])

Since you’ve made a native code change, rebuild and run the app from Xcode. Pick the first mixer and tap Add Rating.

Check the Xcode console. You should see the data you sent reflected in the logs:

2016-09-21 17:49:23.075 [info][tid:com.facebook.react.JavaScript] Running application "AddRatingApp" with appParams: {"rootTag":1,"initialProps":{"currentRating":0,"identifier":1}}. __DEV__ === true, development-level warning are ON, performance optimizations are OFF

Communicating With the Native App

Earlier in this React Native tutorial, you saw how the bridge helped manage communications between native and JavaScript. For example, the View component made JavaScript calls through the bridge to an Objective-C class that eventually displayed a UIView. React Native provides you with hooks to build custom native modules that JavaScript can call.

You’ll have noticed that once the add rating view presents itself, you’re stuck since the Cancel and Save buttons don’t do much. You’ll fix the Cancel button action so it will dismiss the React Native view. To do this, you’ll create a custom native module that will be invoked when you press the button.

Create a new, empty Swift file named AddRatingManager.swift and save it in the Mixer directory. Replace its contents with the following:

import Foundation
import React
 
@objc(AddRatingManager)
class AddRatingManager: NSObject {
 
  var bridge: RCTBridge!
 
  @objc func dismissPresentedViewController(_ reactTag: NSNumber) {
    DispatchQueue.main.async {
      if let view = self.bridge.uiManager.view(forReactTag: reactTag) {
        let presentedViewController: UIViewController! = view.reactViewController()
        presentedViewController.dismiss(animated: true, completion: nil)
      }
    }
  }
}

This class contains dismissPresentedViewController(_:), which takes in a tag tied to the root view. The method then executes code on the main thread, looks for a view registered to that tag and finds the corresponding view controller. It then dismisses the view controller.

Now you’ll create a bridge implementation to make your module known to the bridge.

Create a new, empty Objective-C file named AddRatingManagerBridge.m and save it in the Mixer folder. When prompted, do not create an Objective-C bridging header file.

Add the following content to AddRatingManagerBridge.m:

#import "RCTBridgeModule.h"
 
@interface RCT_EXTERN_MODULE(AddRatingManager, NSObject)
 
RCT_EXTERN_METHOD(dismissPresentedViewController:(nonnull NSNumber *)reactTag)
 
@end

When the bridge initializes, it looks for custom native modules declared via RCT_EXTERN_MODULE and registers them. In the code above, AddRatingManager ends up on a NativeModules list. The bridge is aware of exported methods via the RCT_EXTERN_METHOD declaration.

Your next task is to edit AddRatingApp.js to wire up the Cancel button to calls the native code.

First, import the NativeModules library, like so:

  ...
  NativeModules,
} from 'react-native';

Then, import the AddRatingManager native module by adding the following statement just before the spot where you define the styles:

const { AddRatingManager } = NativeModules;

Modify _renderNavLeftItem to call the dismiss method defined in the native module. Replace onPress with the code below:

onPress={() => {
  AddRatingManager.dismissPresentedViewController(this.props.rootTag);
}}

Note that you’re passing in the rootTag prop that was previously passed during the root view set up. It provides a handle back to the React Native view that’s displayed.

Rebuild the app in Xcode and navigate to your React Native view. Tap Cancel and the React Native view should dismiss:

Mixer cancel action

Adding Save Logic

Create a new JavaScript file named Rating.js and save it in the js directory. Add the following content to the file:

'use strict';
 
import React from 'react';
import ReactNative, {
  StyleSheet,
  Text,
  View,
  Image,
  TouchableOpacity,
} from 'react-native';
 
const styles = StyleSheet.create({
  container: {
    flex: 1,
    marginTop: 100,
    alignItems: 'center',
    backgroundColor: 'white',
  },
  instructions: {
    fontSize: 20,
    color: 'black',
    marginBottom: 20,
  },
  ratings: {
    flexDirection: 'row',
  },
  icon: {
    width: 52,
    height: 58,
    margin: 5
  },
});
 
class Rating extends React.Component {
  // 1
  _onPress(rating) {
    console.log("Rating selected: " + rating);
  }
 
  render() {
    // 2
    var ratings = [];
    for (var k = 1; k <= 5; k++) {
      var key = 'rating-'+k;
      // 3
      var ratingImage = (k <= this.props.rating) ?
        <Image style={styles.icon} source={require('./images/star_on.png')} /> :
        <Image style={styles.icon} source={require('./images/star_off.png')} />;
      // 4
      var rating =
        <TouchableOpacity key={key} onPress={this._onPress.bind(this, k)}>
          {ratingImage}
        </TouchableOpacity>;
      ratings.push(rating);
    }
    // 5
    return (
      <View style={styles.container}>
        <Text style={styles.instructions}>What did you think about this mixer?</Text>
        <View style={styles.ratings}>
          {ratings}
        </View>
      </View>
    );
  }
}
 
module.exports = Rating;

Here are the interesting parts in the above code:

  1. Create a handler that’s invoked when you tap a rating image.
  2. Loop through the maximum number of ratings.
  3. Based on the passed in rating, decide whether to show a highlighted rating image.
  4. Wrap the image in TouchableOpacity to handle touch events.
  5. Return the view with a call to action and the rating images.

Open AddRatingApp.js; you need to configure it to use the new Rating component.

First, import the component:

const Rating = require('./Rating');

Then, replace _renderScene() with the following code:

_renderScene(route, navigator) {
  return (
    <Rating
      title={route.title}
      navigator={navigator}
      rating={this.props.currentRating}
    />
  );
}

Navigate to an add rating view in your simulator. Reload the app and you should see the star ratings. Tap any of the stars:

Mixer add rating view

While you won’t see any star ratings light up, you can verify that Xcode logs the correct selection:

2016-09-21 18:16:03.391 [info][tid:com.facebook.react.JavaScript] Rating selected: 2

Once you’ve finished adding the save logic, the stars should change color to reflect your selection.

You’ll first work on the native side of the save logic by exporting a method you can call from JavaScript.

Open AddRatingManager.swift and add the following code just before the end of the class:

@objc func save(_ reactTag: NSNumber, rating: Int, forIdentifier identifier: Int) -> Void {
  // Save rating
  UserDefaults.standard.set(rating, forKey: "currentRating-\(identifier)")
  dismissPresentedViewController(reactTag)
}

This saves the passed in rating to NSUserDefaults with a key tied to the mixer’s identifier. It then dismisses the view controller by calling the previously defined dismissPresentedViewController(_:).

Next, open AddRatingManagerBridge.m and add the following before the end:

RCT_EXTERN_METHOD(save:(nonnull NSNumber *)reactTag rating:(NSInteger *)rating forIdentifier:(NSInteger *)forIdentifier)

Since you’ve made changes to the native side, rebuild and run the app in Xcode. There should be no changes in behavior at this point.

Mixer Home

Now that the native side is all set up, it’s time to turn your attention to the JavaScript side. Open AddRatingApp.js and add a constructor to store the mixer identifier and rating in the component’s state:

constructor(props) {
  super(props);
  this.state = {
    identifier: props.identifier,
    currentRating: props.currentRating,
  }
}

Next, create the following handler to store the user-selected rating in the currentRating state variable:

onRatingSelected(selectedRating) {
  this.setState({
    currentRating: selectedRating,
  });
}

You’ll call this new method shortly from the Rating component.

In _renderScene, update Rating to look like the following:

<Rating
  title={route.title}
  navigator={navigator}
  rating={this.state.currentRating}
  ratingSelectionHandler={this.onRatingSelected.bind(this)}
/>

Instead of passing in currentRating from props to the Rating component, you pass it in from state. You also pass the in a new prop for the handler that will trigger on a rating selection.

Open Rating.js and modify _onPress to call the newly passed in prop with the selected rating:

_onPress(rating) {
  if (this.props.ratingSelectionHandler) {
    this.props.ratingSelectionHandler(rating);
  }
}

The code passes back the selected rating which updates the current rating state in the parent component. This in turn re-renders the Rating component.

In the simulator, navigate to a mixer and tap Add Rating. Now when you make a selection, the corresponding number of stars are illuminated. Yay!

Mixer add rating active

Time to finish the save action. Open AddRatingApp.js and replace the code that renders the Save button with the following:

_renderNavRightItem(route, navigator, index, navState) {
  if (this.state.currentRating > 0) {
    return (
      <TouchableOpacity
        onPress={() => {
          AddRatingManager.save(
            this.props.rootTag,
            this.state.currentRating,
            this.state.identifier
          );
        }}
        style={styles.navBarRightButton}>
        <Text style={[styles.navBarText, styles.navBarButtonText]}>
          Save
        </Text>
      </TouchableOpacity>
    );
  }
  return null;
}

The Save button will now be hidden until you make a rating selection. The onPress handler now calls the AddRatingManager native module’s save() to save the selection in the native side.

Reload the app, and you’ll notice that the Save button is initially hidden:

Mixer save hidden initially

Select a rating and the Save button should appear. Tap Save, and the view should be dismissed and the rating reflected in the detail view:

Mixer save

Sending Events from the Native App

You can also communicate from native apps to JavaScript through events. The bridge has a dispatcher that can send arbitrary messages to JavaScript. Your JavaScript code can listen to events it’s interested in by subscribing to them.

You’ll send an event back to the React Native app when the save occurs. Open AddRatingManager.swift and modify its subclass:

class AddRatingManager: RCTEventEmitter {

Subclassing RCTEventEmitter allows you to customize the data you send with an event.

Delete the bridge variable declaration to use the superclass variable instead.

Next, override supportedEvents():

override func supportedEvents() -> [String]! {
  return ["AddRatingManagerEvent"]
}

This defines the events that this module will emit.

Then, add the following statement to the end of save(_:rating:forIdentifier):

self.sendEvent(
  withName: "AddRatingManagerEvent",
  body: ["name": "saveRating", "message": rating, "extra": identifier])

This calls the event emitter’s sendEvent(_:body:), passing in a name for the event and JSON representing the message to send. The message you’re sending back corresponds to information about the view and the save parameters.

Open AddRatingApp.js and add code to subscribe to the event. You’ll add the code to the root component even though any component can subscribe to events. First import NativeEventEmitter which handles event subscriptions:

  ...
  NativeEventEmitter,
} from 'react-native';

In the constructor, initialize a private _subscription variable that you’ll use to add and remove event listeners by adding the following after the super(props) statement:

this._subscription = null;

Subscribe to the event in componentDidMount() by adding the following code to the class definition:

componentDidMount() {
  const AddRatingManagerEvent = new NativeEventEmitter(AddRatingManager);
  this._subscription = AddRatingManagerEvent.addListener(
    'AddRatingManagerEvent',
    (info) => {
      console.log(JSON.stringify(info));
    }
  );
}

This code logs the event’s message.

Then, unsubscribe to the event in componentWillUnmount():

componentWillUnmount() {
  this._subscription.remove();
}

Rebuild the project in Xcode. Navigate to a mixer, rate it, and save. Check your Xcode console log, and you should see something similar to the following:

2016-09-21 23:15:45.703 [info][tid:com.facebook.react.JavaScript] {"name":"saveRating","extra":2,"message":3}

Running in Production Mode

There are a few things you need to do to get your app ready for production. So far in this React Native tutorial you’ve been serving the bundle that represents your JavaScript code from a development server. In production, you should use an offline bundle to represent your app.

In Xcode, add a Run Script to the build phase of your target. Go to Targets\Mixer\Build Phases. Click + and select New Run Script Phase. In the newly created Run Script, add the following to the shell script content:

export NODE_BINARY=node
../js/node_modules/react-native/packager/react-native-xcode.sh

Your Xcode project should look like this:

Mixer add run script

Open MixerReactModule.swift and modify sourceURLForBridge(_:) to use the offline bundle that will be generated as part of the build:

func sourceURL(for bridge: RCTBridge!) -> URL! {
  return Bundle.main.url(forResource: "main", withExtension: "jsbundle")
}

From the Xcode menu, go to Product\Scheme\Edit Scheme… and change Build Configuration from Debug to Release.

Run your app; the build may take a little longer than usual to complete. In Xcode, tap the Report Navigator menu in the project bar and select your last build to view the build results. You should see something like the following that shows the offline bundle being written out to main.jsbundle:

Mixer building offline bundle

Note: If you get a build error about being unable to open package.json then edit js/node_modules/react-native/packager/react-native-xcode.sh and replace cd .. with cd ${REACT_NATIVE_DIR}/../..

To test the offline script, kill the development server by hitting Ctrl-C in the Terminal window where you had previously started it. Then in the simulator, navigate to the add rating view for a mixer.

Notice that the bundle loading indicator doesn’t show up and no debug logs are shown in Xcode. You’ll also notice that the load time of the view is much faster.

Mixer running with offline bundle

Where to Go From Here?

Congratulations! as you’ve worked through this React Native tutorial, you’ve learnt how to add React Native to your existing Swift app. You can download the completed project to see the finished version. To run the project, execute the following commands in Terminal:

cd Mixer-Completed/js
npm install
npm start

In another Terminal window run the following:

cd Mixer-Completed/ios
pod install
open Mixer.xcworkspace
Run the project

Note: If you get a build error saying that Xcode can’t find the React lib, then make sure that the Framework Search Path only has $(inherited) set.

Mixer lib error fix step 1

As you get more comfortable with React Native you can look into adding more views.

Up for a challenge? Add a new React Native view to the mixer details view that displays a message when a rating is saved. A few hints to get you going:

  • Think about creating the view using the same methods you did for the add rating view.
  • Consider setting up a new React Native module for this view.
  • You can register the additional module in index.ios.js.

Check out the challenge solution and look for “Challenge” to view the changes.

For more details check out Facebook’s Integrating with Existing Apps tutorial, as well as additional information on Native Modules and Native UI Components.

There’s lots more to learn about communicating between native and React Native and Tadeu Zagallo has an in-depth article on Bridging in React Native that’s worth a read.

If you have any questions or comments, please join the discussion below!

Christine Abernathy

Christine is an Engineer on the Developer Advocacy team at Facebook. In this role, she is focused on helping grow the mobile developer ecosystem with emphasis on Android, iOS, and the mobile web. Prior to Facebook, Christine headed up engineering at Mshift, a mobile banking software provider, delivering iPhone apps and mobile browser-based products.

You can find Christine on Facebook.

Other Items of Interest

Save time.
Learn more with our video courses.

raywenderlich.com Weekly

Sign up to receive the latest tutorials from raywenderlich.com each week, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

PragmaConf 2016 Come check out Alt U

Our Books

Our Team

Video Team

... 20 total!

Swift Team

... 16 total!

iOS Team

... 29 total!

Android Team

... 15 total!

macOS Team

... 10 total!

Apple Game Frameworks Team

... 11 total!

Unity Team

... 11 total!

Articles Team

... 11 total!

Resident Authors Team

... 15 total!