DEV Community

Cover image for Zero to App Store in 30 Days 🚀
Henry Arbolaez
Henry Arbolaez

Posted on

Zero to App Store in 30 Days 🚀

This post is a high-level technical overview of how we integrated React Native (RN) into Course Hero's flagship iOS app. Our first RN app, for our Textbook Solutions product, is now out in the wild.

The idea to implement RN came from a Course Hero internal hackathon project done by myself and Ernesto Rodriguez. We saw the opportunity to introduce Course Hero to this great technology, already in use at Shopify, Facebook, Instagram, Tesla, and more.

Though Course Hero currently uses React for web development, we also have separate mobile teams who maintain our mobile native apps. Using RN allowed web developers with good knowledge of React to apply their expertise towards building a mobile app. This flexibility allowed us to scale our textbook product to native platforms in order to give our customers a great experience.

Deep dive on the integration

RN Dependencies

When we started, we had a separate repository on GitLab: one for our web app, and another for our iOS app. We created a separate repository for the RN integration, which had the build file. There is no easy way of creating the two links, other than having them in a remote somewhere and fetching the build from a script inside the iOS repo or adding the RN inside the iOS repo. But we didn't want the iOS team needing to clone any RN dependencies, and this was our first iteration anyway.

We started by adding the RN dependencies to the iOS Podfile. We then forked the RN project to our Course Hero Github Repo and then used the source method to clone the RN project to our local ~/.cocoapods/repos/coursehero dir. Now everyone who clones the iOS repo will automatically have the RN dependencies when doing pod install.

In Github, we made 0.63-stable our default branch. This helped us keep the RN project in sync with the Podfile. To change the default branch in GitHub: [repo] -> Settings -> Branches

# Podfile
def react_native_pods
  source 'https://github.com/coursehero/react-native.git'
  source 'https://github.com/CocoaPods/Specs.git'
  rn_path = '~/.cocoapods/repos/coursehero'
  # Default RN depencences
  pod 'React', :path => "#{rn_path}/"
  pod 'React-Core', :path => "#{rn_path}/"
  pod 'React-Core/DevSupport', :path => "#{rn_path}/"
  pod 'React-Core/RCTWebSocket', :path => "#{rn_path}/"
  
  # 3rd party
  pod 'glog', :podspec => "#{rn_path}/third-party-podspecs/glog.podspec"
  # … all the other depencies that your project needs
end

def main_pods
  # … additional pods
  react_native_pods
end

abstract_target 'All Targets' do
  target 'Course Hero' do
    project 'Course Hero.xcodeproj'
    main_pods
  end
end
Enter fullscreen mode Exit fullscreen mode

Our Podfile will start to look something like this - react_native_pods been the method that encapsulates all the RN dependencies

Intro to RCTRootView

Doing the integration between the two sides is fairly simple. In iOS, we can use the subclass RCTRootView from the UIView class, which we can use in any location of our iOS app.

Most of all the Swift and the Obj-c code below is under the CourseHero iOS folder. CourseHero/ReactNative/Textbooks/

// RNViewManager.swift
class RNViewManager: NSObject {
  static let sharedObject = RNViewManager()
    var bridge: RCTBridge?
    // crating the bridge if is necesary, avoding creating multiple instances

  func createBridgeIfIsNeeded() -> RCTBridge {
    if bridge == nil {
      bridge = RCTBridge.init(delegate: self, launchOptions: nil)
    }
    return bridge!
  }

  func viewForModule(_ moduleName: String, initialProperties: [String : Any]?) -> RCTRootView {
    let viewBridge = self.createBridgeIfIsNeeded()
    let sourceURL = Bundle.main.url(forResource: "main", withExtension: "jsbundle")
    #if DEBUG
      sourceURL = URL(string: "http://localhost:8081/index.bundle?platform=ios")
    #endif

    let rootView: RCTRootView = RCTRootView(
      bundleURL: sourceURL,
      bridge: viewBridge,
      moduleName: moduleName, // the module name, this is the name of the React Native App
      initialProperties: initialProperties
    )
    return rootView
  }
}
Enter fullscreen mode Exit fullscreen mode

RNViewManager is going to a swift reusable class

// CourseHeroHomeController
extension CourseHeroHomeController {
  func openTextbookApp() {
    let textbookRNView = RNViewManager.sharedObject.viewForModule(
    "TextbookApp", // the RN app name
    initialProperties: nil)
    let reactNativeVC = UIViewController()
    reactNativeVC.view = textbookRNView
    // differnt settings for our need case
    reactNativeVC.modalPresentationStyle = .fullScreen
    self.present(reactNativeVC, animated: true, completion: nil)
  }
}
Enter fullscreen mode Exit fullscreen mode

Calling RNViewManager class

How do the two worlds communicate?

For the RN and native applications to communicate, we need a bridge - a way to send JSON data bidirectionally and asynchronously.

In our case, the RN app had a few modules that we needed to implement. From sending user information to sending callbacks and executing some business logic on the native side.

RN To Native

A key step in the process was creating a Native Module, which is a 3 step process.

The first step is to tell our native app about the RN bridge (we only need to perform this once), and then add the data below to the header.h file in our project. Note that there should only be one header file per project and it should conform to the standard naming convention, ProjectApp-Bridging-Header.h

// CourseHero-Bridging-Header.h
//...
#import <React/RCTBridgeModule.h>
#import <React/RCTViewManager.h>
#import <React/RCTUIManager.h>
#import <React/RCTBridge.h>
#import <React/RCTRootView.h>
Enter fullscreen mode Exit fullscreen mode

Can be found in the Build Settings tab too

Next, we create our Modules. We started with TrackingModule.swift which allowed us to access the Native code from the RN side and report some tracking metrics to our internal tracking service.

import Foundation
import React

@objc(RNTrackingModule)
class RNTrackingModule: NSObject {

  @objc static func requiresMainQueueSetup() -> Bool {
    // true will initialized the class on the main thread
    // false will initialized the class on the background thread 
    return true
  }

  // all method that will need to be accessed by Obj-C
  // needs to add the `@objc` directive
  @objc func logEvent(_ eventName: String, withTrackInfo: [String: Any]) -> Void {
    // log events to your tracking service
    CHTrackingService.logEvent(eventName, withValues: withTrackInfo)
  }


  @objc
  // constantsToExport: A native module can export constants that are immediately available to React Native at runtime. 
  // This is useful for communicating static data that would otherwise require a round-trip through the bridge.
  // this data is on runtime, you won't get updated values.
  func constantsToExport() -> [AnyHashable: Any]! {
    return [
      "inititalData": ["userId": 1],
    ]   
  }
}
Enter fullscreen mode Exit fullscreen mode

Finally, we exposed the Swift class Module to RN by creating another file, typically the same name of the module above but with a .m extension representing Objective-C. This is typically referred to as an RN Macro.

//
//  RNTrackingModule.m
//  Course Hero
//
//  Created by Henry Arbolaez on 01/25/21.
//  Copyright © 2021 Course Hero. All rights reserved.
//

#import <React/RCTBridgeModule.h>

// RCT_EXTERN_REMAP_MODULE allow to rename the exported module under a different name
// first arg is the name exposed to React Native
// second arg is the Swift Class
// third arg is the superclas
@interface RCT_EXTERN_REMAP_MODULE(TrackingModule, RNTrackingModule, NSObject)

RCT_EXTERN_METHOD(logEvent: (NSString *)eventName withTrackInfo:(NSDictionary *)withTrackInfo)

@end
Enter fullscreen mode Exit fullscreen mode

Accessing the Swift module from React Native

With the native side set up, we moved to the RN project/App.js file, where we imported NativeModules from the react-native package. Any module exported from the Obj-C Macros will be available using the NativeModules object.

// App.js
import { NativeModules } from 'react-native'

// now we should have access to the logEvent and initialData
console.log(NativeModules.TrackingModule)
Enter fullscreen mode Exit fullscreen mode

To recap, the process of creating a Native Module and exposing it to RN goes like this:

1. Create the Swift Module Class
2. Obj-C Macro which expose the Swift Module Class
3. NativeModules which is used in RN app, to access the module or methods exported from Objective-C

* @objc in the top of a swift method, is to export them to the Objective-C Class
* RCT_EXTERN_MODULE or RCT_EXPORT_MODULE (from objective-c code) - to export the module or methods to the RN
Enter fullscreen mode Exit fullscreen mode

Native To React Native

When we instantiate RCTRootView, we can pass data into the initialProperties parameter. The data needs to be an NSDictionary, which then gets converted to a JSON object that we can access in the root component.

let textbookRNView = RNViewManager.sharedObject.viewForModule(
  "TextbookApp", // the RN app name
  initialProperties: [ "currentUser": currentUser];
)
Enter fullscreen mode Exit fullscreen mode

when we load the RN app, it adds a rootTag, which allows us to identifier the RCTRootView

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

interface Props {
  currentUser: User
  rootTag: number
}

const App = ({ currentUser, rootTag }: Props) => {
  return (
    <View>
      <Text>Hi, {currentUser.name}!</Text>
      <Image source={{ uri: currentUser.profileUrl }} />
    </View>
  )
}
Enter fullscreen mode Exit fullscreen mode

Destrcuring props, which have current

UserRCTRootView exposes another way of sending messages by using appProperties, which is helpful if you want to update the properties initialized in your RCTRootView and trigger a rerender of the root component.

We didn't have a use case for using the RCTEventEmitter subclass, but this is the preferred way of emitting some events to signal that something has changed to the RN side.

Iteration Speed

RN allowed us to build, integrate and deploy the textbook app to the existing iOS app in less than a month. While doing the integration, we took advantage of hot reloading which allowed us to see the changes being made in RN almost instantly, compared to >20 seconds that native code would typically take to build.

Summary

By putting just a little effort to integrate React Native into our application stack, we quickly realized the advantages it would bring to our organization. There may be cases where React Native is not the right choice, but for us, it works great for our Textbook Solutions product and we look forward to building others using this technology. We hope this summary helps you get started on your React Native integration journey.

Originally posted at Course Hero Engineering Blog


We're hiring!

Discussion (0)