React Native

Add React Native to the Signal open source app — part 1 (iOS)

Mariusz StaniszNov 20, 20246 min read

Lately, I’m into brownfield challenges. I like to take an open-source app and try to run React Native in there. This time I decided to do it with Signal — a chat application famous for its security features.

In this article, I’m going to show you the steps I had to take in order to make the app work. The thing about the brownfield integration is that with every app it’s different. That’s why I believe that a couple of case studies alongside React Native documentation will help you to understand how to do it in your project. Let’s get started then!

React Native setup

In the beginning, I created a clean directory react-native-signal, and initialized a git repository there:

mkdir react-native-signal cd react-native-signal git init

At this point, I was ready to create some React Native files! There are exactly four files that I had to add:

  • package.json — needed to install JS dependencies of React Native. It also includes some useful scripts to work with a React Native application — source code
  • metro.config.js — necessary to set up Metro bundler and have access to the hot reload feature — source code
  • index.js — entry point for the React Native application, it registers the app’s name so it can be found by the native code — source code
  • App.tsx — basic UI template written with React — source code

You don’t have to write them on your own — actually I’ve taken the files directly from the React Native Community template to make the setup easier.

And well, that was it! Now you can run yarn install or npm install, and we can move on to the native code!

iOS build

For the start, I cloned the Signal-iOS repository from my fork into the ios directory by creating a git submodule:

git submodule add https://github.com/<YOUR_GITHUB_HANDLE>/Signal-iOS.git ios

Let’s get into the files we’ve just cloned! Since React Native uses some Ruby dependencies I had to take care of them. To align the Gemfile with the React Native documentation I had to make a few small changes:

DEPENDENCIES
+ activesupport (>= 6.1.7.5, != 7.1.0)
anbt-sql-formatter
- cocoapods
+ cocoapods (>= 1.13, != 1.15.1, != 1.15.0)
fastlane
xcode-install
+ xcodeproj (< 1.26.0

Now, I was ready to move on to the Podfile. In the case of Signal, it was a bit different, since the Pods — iOS native dependencies — were pushed to a remote repository. It is pretty common in the iOS world, but for React Native developers it may be a bit confusing. That’s why in the beginning I decided to remove the git submodule pointed to Pods, and add the whole directory to .gitignore. At this point, I decided to update the platform target, and add a couple of methods there:

- platform :ios, '15.0'
+ require Pod::Executable.execute_command('node', ['-p',
+ 'require.resolve(
+ "react-native/scripts/react_native_pods.rb",
+ {paths: [process.argv[1]]},
+ )', __dir__]).strip
+ prepare_react_native_project!
+ platform :ios, 15.1
target 'Signal' do
project 'Signal.xcodeproj', 'Debug' => :debug, 'Release' => :release
+ use_native_modules!
+ use_react_native!
# Pods only available inside the main Signal app
ui_pods
post_install do |installer|
+ react_native_post_install(installer)
enable_strip(installer)
enable_extension_support_for_purelayout(installer)

If you compare this code to the React Native documentation you may notice that the methods I’ve added here are a bit different from the recommended ones. In fact, use_react_native! and react_native_post_install both don’t require additional arguments, since we can use their default values — that’s why the added code looks much simpler!

At this point, I was ready to see this magical command execute successfully:

bundle exec pod install

It meant I could dive into some Swift files to write the implementation. Let’s begin with the root of all iOS applications — the AppDelegate!

Wiring

I would say it can be the most challenging file of the whole brownfield integration because we must inherit from RCTAppDelegate to make the app work. Well, there are some workarounds, but if you decide to take this path the integration may become even more difficult.

The inheritance from RCTAppDelegate basically means that we would have to override a bunch of methods of the original AppDelegate. In fact, for many of them, you won’t need to call super, but if it was a production integration I would thoroughly investigate each method to see if I accidentally don’t break any functionality of the app. However, since we’re talking about adding a simple React Native screen I just decided to override all necessary methods without changing them. There was one exception, though. It’s the application(_:didFinishLaunchingWithOptions:):

+ import React_RCTAppDelegate

@main
- final class AppDelegate: UIResponder, UIApplicationDelegate {
+ final class AppDelegate: RCTAppDelegate {
- func application(
+ override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
+ self.automaticallyLoadReactNativeWindow = false
+ super.application(application, didFinishLaunchingWithOptions: launchOptions)
let launchStartedAt = CACurrentMediaTime()

Not too bad, is it? Inheritance forces us to do one more thing, though. I had to implement two methods: sourceURL(for bridge: RCTBridge) and bundleUrl. Luckily in this case we can stick to the React Native documentation, and copy the code straight from there:

+ override func sourceURL(for bridge: RCTBridge) -> URL? {
+ self.bundleURL()
+ }
+ override func bundleURL() -> URL? {
+ #if DEBUG
+ RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
+ #else
+ Bundle.main.url(forResource: "main", withExtension: "jsbundle")
+ #endif
+ }

Before I could run the Metro bundler successfully I had to do one more update in the Signal-info.plist file:

<key>UIViewControllerBasedStatusBarAppearance</key>
- <true/>
+ <false/>

Final touches

At this point I’ve had all the necessary components wired, so I was ready to choose the right spot to open React Native! In order to do that I decided to swap the logic of the “new chat” button on the home screen in ChatListViewController.swift file:

@objc
func showNewConversationView() {
AssertIsOnMainThread()
Logger.info("")
// Dismiss any message actions if they're presented
conversationSplitViewController?.selectedConversationViewController?.dismissMessageContextMenu(animated: true)
- let viewController = ComposeViewController()
- SSKEnvironment.shared.contactManagerImplRef.requestSystemContactsOnce { error in
- if let error {
- Logger.error("Error when requesting contacts: \(error)")
- }
- // Even if there is an error fetching contacts we proceed to the next screen.
- // As the compose view will present the proper thing depending on contact access.
- //
- // We just want to make sure contact access is *complete* before showing the compose
- // screen to avoid flicker.
- let modal = OWSNavigationController(rootViewController: viewController)
- self.navigationController?.presentFormSheet(modal, animated: true)
- }
+ let reactNativeViewController = UIViewController()
+ let factory = (UIApplication.shared.delegate as! RCTAppDelegate).rootViewFactory
+ reactNativeViewController.view = factory.view(withModuleName: "HelloBrownfield")
+ self.present(reactNativeViewController, animated: true)
}

In order to make the app look better I’ve also swapped the “new chat” icon to a React Native logo and updated one string. I’ll be honest — I’ve done it just because I wanted the whole integration to look nicer on a demo. And that is it! At this point everything was ready, so I could build the app, run the Metro bundler via npm run start, and enjoy the hot reload at its finest!

We’re Software Mansion: React Native core contributors, New architecture experts, community builders, multimedia experts, and software development consultants. Do you need help with building your brownfield app? Hire us: [email protected].