Integrating React Modules Into an Existing iOS App

August 31, 2016 | Mobile

In our previous blog post, we looked at some of React Native's potential benefits. We also experimented with native components by integrating UITableView into a React view. In this post, we're going to take a different approach and integrate a React Native module into an existing app. We'll do this by creating a UIViewController subclass called ReactViewController that will load an instance of RCTRootView. RCTRootView is a UIView subclass provided by React Native that can load a React module. It will do most of the real work for us so ReactViewController will be very simple.

1. ReactTarget

Before we start writing ReactViewController, we need a way to specify where our React modules are located. The location will vary depending on whether your app runs in the simulator or on a device. In the simulator, the app should load the modules from the React development server which is typically running at localhost:8081. On the device, the app should load the modules from the main.jsbundle file that is bundled with your application at build time. If you create a new application from the command line using react-native init, React Native will create variables in the app delegate's application:didFinishLaunchingWithOptions: that you can uncomment to choose the appropriate location for your build. That's fine for a regular React Native app that will only have one instance of RCTRootView for the entire application, but we may create multiple instances of ReactViewController. We need a class that will manage the module location globally so we can update it in one place without editing the ReactViewController code. We'll call that class ReactTarget:

// ReactTarget.swift

import Foundation

enum ReactTargetType {
    case Simulator
    case Device
}

class ReactTarget { 
    static var type = ReactTargetType.Device
    static var simulatorBaseUrl = NSURL(string: "http://localhost:8081/index.ios.bundle?platform=ios")
    static var deviceBaseUrl = NSBundle.mainBundle().URLForResource("main", withExtension: "jsbundle")

    static var baseUrl: NSURL? {
        get {
            return (self.type == .Simulator) ? self.simulatorBaseUrl : self.deviceBaseUrl
        }
    }
}

ReactTarget declares three static variables and a static property:

  • type is a ReactTargetType enum that describes whether the app is running in the simulator or on a device. The default is .Device.
  • simulatorBaseUrl is an NSURL that points to the React development server. You usually won't have to change this, but you can if your server is running on a different host or port.
  • deviceBaseUrl is an NSURL that points to the location of the main.jsbundle file. Again, you usually won't have to change this.
  • baseUrl is a static property that returns either simulatorBaseUrl or deviceBaseUrl, depending on type.

You can set ReactTarget.type anywhere in the application. The app delegate's application:didFinishLaunchingWithOptions: method is convenient. Just remember to change this when you switch between the simulator and device:

// AppDelegate.swift

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    // Override point for customization after application launch.
    ReactTarget.type = .Simulator

    return true
}

As a side note, you may recall from my previous post that React Native requires you to write components in Objective C. So why are we using Swift here? Simply put, neither ReactTarget nor ReactViewController is a React Native component! They are separate classes that are totally unaffected by React Native's inner workings, so we're free to use Swift if we want to.

2. ReactViewController

As promised, ReactViewController is very simple:

import UIKit

class ReactViewController: UIViewController {
    @IBInspectable var moduleName: String?

    var initialProperties: [NSObject: AnyObject]?
    var launchOptions: [NSObject: AnyObject]?

    private(set) var rootView: RCTRootView?

    override func viewDidLoad() {
        super.viewDidLoad()
        self.configure()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    func setPropertyValue(value: AnyObject, forKey key: NSObject) {
        var properties: [NSObject: AnyObject] = self.rootView?.appProperties ?? [:]
        properties[key] = value
        self.rootView?.appProperties = properties
    }

    private func configure() {
        guard let module = self.moduleName else {
            print("ReactViewController \(self) requires a module name.")
            return
        }

        self.rootView = RCTRootView(
            bundleURL: ReactTarget.baseUrl,
            moduleName: module,
            initialProperties: self.initialProperties,
            launchOptions: self.launchOptions)

        if let rootView = self.rootView {
            self.view.addSubview(rootView)

            rootView.translatesAutoresizingMaskIntoConstraints = false
            rootView.frame = self.view.bounds

            self.view.addConstraints([
                rootView.topAnchor.constraintEqualToAnchor(self.view.topAnchor),
                rootView.bottomAnchor.constraintEqualToAnchor(self.view.bottomAnchor),
                rootView.leftAnchor.constraintEqualToAnchor(self.view.leftAnchor),
                rootView.rightAnchor.constraintEqualToAnchor(self.view.rightAnchor)
            ])
        }
    }
}

The moduleName instance variable is @IBInspectable so you can set the module name in a UIStoryboard. This is all you have to do to load a module that doesn't require initial properties or launch options. Simply create an instance of ReactViewController, set the moduleName to the name of the root module that you want to load and run your app. The view controller's viewDidLoad method will call configure, which will configure an instance of RCTRootView, which will in turn load the specified module. You can use as many instances of ReactViewController in your app as memory allows.

If your module requires initial properties or launch options, you can set them in code before the view controller loads its view:

// Arbitrary ReactTableView subclass:

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    self.initialProperties = ["text": "Hello World!"]
}

// React module (JavaScript):

render() {
    return (
        <View>
            <Text>{this.props.text}</Text> // "Hello World!"
        </View>
    );
}

RCTRootView provides an instance variable called appProperties, which is a dictionary that RCTRootView will pass to the module as props. You can update data at any time by assigning a new dictionary to appProperties:

// Arbitrary ReactTableView subclass:

self.rootView.appProperties = ["text", "Hello React!"]

// React module (JavaScript):

render() {
    return (
        <View>
            <Text>{this.props.text}</Text> // "Hello React!"
        </View>
    );
}

RCTRootView won't let you set values for individual keys in appProperties but ReactViewController provides a convenient workaround. The setPropertyValue:forKey: method makes a copy of rootView.appProperties, updates the copy, then assigns the copy to rootView.appProperties. We still have to replace the entire appProperties dictionary, but setPropertyValue:forKey: makes it a bit easier to work with a dictionary that contains multiple key-value pairs:

// Arbitrary ReactTableView subclass:

self.setPropertyValue("Hello Again!" forKey: "text")

// React module (JavaScript):

render() {
    return (
        <View>
            <Text>{this.props.text}</Text> // "Hello Again!"
        </View>
    );
}

This can be a great way to accelerate UI development by taking advantage of React's powerful declarative syntax without abandoning your existing Swift or Objective-C codebase. You can freely mix this technique with other UIKit components. For example, a master-detail app could use a regular UITableView for the master list, but use React Native to implement a complex detail view. You can even embed ReactViewController in a container view, so you can integrate a React module into an existing UI built in a storyboard. Even if you can't or don't want to use React Native to build your entire app, it can still be a very useful addition to your UI toolbox.


We are DevBBQ.
We are digital product creators with big appetites. For Hire.

Arrow circle down@2x 6412eee90b778a7dc0698b617fc13fb6c19fdd10f53c4479172d38bc5ce4e168
Therapia white 55d4657b58374a27a9634c3cb6db71c5efd7179c0fca9530931b0a015c78fade

At-home physiotherapy social network, appointment bookings, and payment processing.

Cooked up by DevBBQ

 

Contact Us Lets chat@2x 9e08eee888b6926761c1dfe33e7c84fb2f0345e4be974f2d9e4dca9c39d24980

 

DEVBBQ INCORPORATED