Integrating React Native into an existing Swift project

By Vagmi on 2 Mar 2016

React Native is really nice. Its Cmd-R style development is refreshing compared to XCode's build, deploy and run cycle. But if you want to use things like CoreData, NSOperationQueues or any of the other awesome libraries in the iOS world, you have to write bridges. In this blog post we will be focusing on adding React Native to an existing swift application. React Native's secret is in its RCTRootView. So just like for the browser, React focuses on being a good view library and can be mixed with your existing swift application.

We'll be using the XCode's Single View Controller Swift project template for demonstration. React Native's site has excellent documentation how you would go about such an integration. I add a few more things on top like loading from compiled / hosted JSBundle, caching the JSBundle and manually reloading React code after refreshing the cache. You can download the sample code from Github.

After you have your project and set it up with cocoapods, go ahead and initialize your package.json. My package.json looks like this. I have placed my package.json in the same location as my .xcodeproj and .xcworkspace

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
  "name": "integrate-rn",
  "version": "1.0.0",
  "description": "",
  "scripts": {
    "start": "node node_modules/react-native/local-cli/cli.js start",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "react-native": "^0.21.0"
  }
}

Then go ahead and run npm install on the command line to install the necessary dependencies. This will create a node_modules folder. You can add this folder to your .gitignore file much like Pods directory for cocoapods.

You would then have to add React and its subspecs to your Podfile. My Podfile looks like this.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '8.0'
use_frameworks!

target 'IntegrateRN' do
  pod 'React', :path => './node_modules/react-native', :subspecs => [
    'Core',
    'RCTImage',
    'RCTNetwork',
    'RCTText',
    'RCTWebSocket',
    # Add any other subspecs you want to use in your project
  ]
  pod 'Alamofire'
  pod 'SnapKit'
end

This adds references to the React Native pods and links them to your project as frameworks. Don't forget to pod install after this. You can then add the entry point to the react native view. This file will be called index.ios.js by convention and we will be referring to it from our ViewController.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
'use strict';

import React, {
  Text,
  View
} from 'react-native';

var styles = React.StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: 'gray'
  }
});

class SimpleApp extends React.Component {
  render() {
    return (
      <View style={styles.container}>
        <Text>This is a React Native app</Text>
      </View>
    )
  }
}

React.AppRegistry.registerComponent('SimpleApp', () => SimpleApp);

You can then start the react-native server by running npm start at the command line from the folder that has the file package.json. This should start a server that should be able to serve our react native bundle from localhost:8081.

I have used a simple View controller which has two views a RCTRootView that hosts the React Native app and a UIButton that will let me reload the code using the RCTBridge object of the RCTRootView. This is what my ViewController code looks like.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import UIKit
import React
import SnapKit

class ViewController: UIViewController {
  var rootView:RCTRootView!
  
  override func viewDidLoad() {
    super.viewDidLoad()

    let button = UIButton(type: .System)
    button.setTitle("Refresh", forState: .Normal)
    button.addTarget(self, action: "reload", forControlEvents: .TouchUpInside)

    view.addSubview(button)
    
    button.snp_makeConstraints { make in
      make.top.equalTo(view.snp_top).offset(20)
      make.left.equalTo(view.snp_left)
    }

    let codeLocation = NSURL(string: "http://localhost:8081/index.ios.bundle?platform=ios")!
    rootView = RCTRootView(bundleURL: codeLocation, moduleName: "SimpleApp", initialProperties: nil, launchOptions: nil)
    view.addSubview(rootView)

    rootView.snp_makeConstraints { make in
      make.top.equalTo(button.snp_bottom).offset(8)
      make.left.equalTo(view.snp_left)
      make.right.equalTo(view.snp_right)
      make.bottom.equalTo(view.snp_bottom)
    }
  }

  func reload() {
    rootView.bridge.reload()
  }
  override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
  }
}

Running the app should present the app with React Native view loaded.##Bridging Swift views to React Native

Now this turned out to be trickier than I originally thought it would. React Native has a RCTViewManager class that expects you to return a View. Now the view can be configured with properties and you would have to export view using Obj-C macros. For the purposes of illustration, I am going to create a simple view that has a label and takes in property called message. It would then simply display the value of the message in a label concatenated with the word "Swifted". Yeah… i made that nonsensical term up just now. I am going to create a view called CustomRNView.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import UIKit
import React
import SnapKit

@objc(CustomRNView)
class CustomRNView : UIView {
  
  var lblMessage:UILabel!
  
  override init(frame: CGRect) {
    super.init(frame: frame)
    self.frame = frame
    
    lblMessage = UILabel()
    lblMessage.text = "initial message"
    self.addSubview(lblMessage)
    lblMessage.snp_makeConstraints { make in
      make.top.equalTo(self.snp_top).offset(8)
      make.left.equalTo(self.snp_left).offset(8)
    }
  }

  required init?(coder aDecoder: NSCoder) {
      fatalError("init(coder:) has not been implemented")
  }
  func setMessage(message: NSString) {
    lblMessage.text = "\(message) Swifted"
  }
}

Notice that I am exposing the swift class to the ObjC world using the @objc annotation. Now I have to write a RCTViewManager class that returns me this view.

1
2
3
4
5
6
7
8
import React

@objc(CustomRNViewManager)
class CustomRNViewManager : RCTViewManager {
  override func view() -> UIView! {
    return CustomRNView()
  }
}

This class CustomRNViewManager simply has a view method that returns a CustomRNView object. Also note that this class has also been annotated with the @objc annotation.

Now we have to expose it to the react world with the the RCT_EXTERN_MODULE and RCT_EXPORT_VIEW_PROPERTY macros. You will have to create the CustomRNView.h and CustomRNView.m files. The header declares our custom UIView as a RCTView and the implementation will export our CustomRNViewManager.

CustomRNView.h
1
2
3
4
5
#import "React/RCTView.h"

@interface CustomRNView : RCTView
@property (nonatomic, assign) NSString *message;
@end
CustomRNView.m
1
2
3
4
5
6
7
8
#import "CustomRNView.h"
#import "React/RCTViewManager.h"

@interface RCT_EXTERN_MODULE(CustomRNViewManager, RCTViewManager)

RCT_EXPORT_VIEW_PROPERTY(message, NSString)

@end

You do not have to put anything in the bridging header as we are loading rest of the React Native specs as frameworks.

Now, we have to get our CustomRNView to our javascript side. You can now use the requireNativeComponent method to refer to the exported module.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
'use strict';

import React, {
  requireNativeComponent,
  Text,
  View
} from 'react-native';


var CustomRNView = requireNativeComponent('CustomRNView',null);

var styles = React.StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#ffefef",
    padding: 8
  }
});

class SimpleApp extends React.Component {
  render() {
    return (
      <View style={styles.container}>
        <Text>This is a React Native app.</Text>
        <CustomRNView message="message will be"/>
      </View>
    )
  }
}

React.AppRegistry.registerComponent('SimpleApp', () => SimpleApp);

It should now display the CustomRNView within the react native view.

Caching the jsbundle

For the performance reasons, you might want to distribute the bundle with your app or better still load it the first time from a URL and then cache it to your documents directory. When you want to refresh then you can redownload the bundle and reload the code via RCTBridge. I have done precisely that in my ViewController.swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import UIKit
import React
import SnapKit
import Alamofire

class ViewController: UIViewController {
  
  static let CODE_URL =  "https://5b2cb626.ngrok.com/index.ios.bundle?platform=ios"
  // or where ever you are serving the bundle from
  
  var rootView:RCTRootView!
  var button:UIButton!
  
  override func viewDidLoad() {
    super.viewDidLoad()
    self.edgesForExtendedLayout = .None
    
    button = UIButton(type: .System)
    button.setTitle("Refresh", forState: .Normal)
    button.addTarget(self, action: "reload", forControlEvents: .TouchUpInside)
    view.addSubview(button)
    
    button.snp_makeConstraints { make in
      make.top.equalTo(view.snp_top).offset(20)
      make.left.equalTo(view.snp_left)
    }
    let paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)
    let codePath = paths[0] + "/code.js"
    if(NSFileManager.defaultManager().fileExistsAtPath(codePath)) {
      loadReactView(codePath)
    } else {
      saveCodePath { self.loadReactView(codePath) }
    }
  }
  
  func loadReactView(codePath: String) {
    rootView = RCTRootView(bundleURL: NSURL(fileURLWithPath: codePath), moduleName: "SimpleApp", initialProperties: nil, launchOptions: nil)
    view.addSubview(rootView)
    rootView.snp_makeConstraints { make in
      make.top.equalTo(self.button.snp_bottom).offset(8)
      make.left.equalTo(view.snp_left)
      make.right.equalTo(view.snp_right)
      make.bottom.equalTo(view.snp_bottom)
    }
  }
  
  func reload() {
    saveCodePath { self.rootView.bridge.reload() }
  }
  
  func saveCodePath(completionHandler: ()->Void) {
    download(Method.GET, ViewController.CODE_URL) { (_,_) in
      let docDirURL = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)[0]
      let codeURL = docDirURL.URLByAppendingPathComponent("code.js")
      // delete file if exists
      try? NSFileManager.defaultManager().removeItemAtURL(codeURL)
      return codeURL
      }.response { (_,_,_,err) in
        if let error = err {
          NSLog("encountered an error \(error)")
        } else {
          completionHandler()
        }
    }
  }
  
  override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
  }
}

This will download the the file once and load it from the documents directory. You will also notice that the app launch time is significantly faster. You might want to consider using services like Microsoft CodePush to automatically download the latest code as you push changes to your code. We are very excited with this approach as we can be more agile with our existing swift apps by leveraging React Native and its code push strategies without having to go through the app compile, deploy and run cycles. Let us know your thoughts and opinions.

comments powered by Disqus