Building React Native Apps

We’ve been using and loving React Native as noted in my previous post. As we are working towards rolling out a fully-featured app, one thing that needed solved was how we should build the app for different environments. For example, how can we make (slightly different) development, staging, and production builds?

In a Github issue, I ran into a few other people also wondering how to do this, so I’ve added a few ways to the Example App to show the approaches we are using.

The three approaches we are trying out are:

  • Configurations
  • Compile Flags
  • Run Variables

Environments

I had already added the Environment model and EnvironmentStore store to the project. However, I just added actual dynamic configurations to the XCode project and the EnvironmentManager code is using that.

Here is the staging config:

1
2
3
#include "Pods/Target Support Files/Pods/Pods.staging.xcconfig"

GCC_PREPROCESSOR_DEFINITIONS = $(inherited) kEnvironment="@\"staging\""

That gets used by the EnvironmentManager to send that, along with other data, over to JS land.

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
RCT_EXPORT_METHOD(get:(RCTResponseSenderBlock)callback)
{
  NSString *locale = [[NSLocale currentLocale] localeIdentifier];
  locale = [locale stringByReplacingOccurrencesOfString:@"_" withString:@"-"];

  NSNumber * simulator = @NO;
  NSString * version = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"];
  NSString * buildCode = [[NSBundle mainBundle] objectForInfoDictionaryKey:(NSString *)kCFBundleVersionKey];

  NSString * envName = kEnvironment;
  NSDictionary *passed = [[NSProcessInfo processInfo] environment];
  NSString *override = [passed valueForKey:@"SAMPLE_ENV"];
  if (override) {
    envName = override;
  }
#ifdef TEST_ENVIRONMENT
  envName = @"test";
#endif
#ifdef STAGING_ENVIRONMENT
  envName = @"staging";
#endif
  
  
#if TARGET_IPHONE_SIMULATOR
  simulator = @YES;
#endif

  callback(@[ @{
                @"name": envName,
                @"buildCode": buildCode,
                @"simulator": simulator,
                @"version": version,
                @"locale": locale
            }]);
}

By naming a scheme, that uses this config, we can launch the app knowing that its world is slightly different as determined by the Environment model. For example:

1
2
3
4
5
6
7
8
9
10
11
12
Model.prototype.getApiHost = function() {
  switch(this.data.name) {
    case 'test':
      return 'http://localhost:3001';
    case 'debug':
      return 'http://localhost:3000';
    case 'staging':
      return 'https://someday.herokuapp.com';
    default:
      throw("Unknown Environment.getApiHost: " + this.data.name);
  }
};

By the way, I also added some of the other information from Objective-C like the version, build code, locale, and whether we’re running in the simulator. We’ve found a use for all of those in our Javascript code.

Configurations

This new build is using the kEnvironment from our custom xcconfig files as seen above. So we can pass in the environment name via configuration.

XCode has these schemes that set up the configurations. The issue I wrote up was lamenting the fact that all the of the child projects (like React Native) have the use the same name (“Debug” or “Release”) for it to work as expected. For example, I can’t really have a configuration called “Staging” that gets all the good stuff from the “Debug” configurations.

I’ve more or less just accepted this and moved on. Our “Staging” and “Production” configurations just end up using the default (“Release”) configurations from all the children. That’s working well enough. The the other two approaches are ways to mitigate this issue, though. So when I said I accepted it, I guess that’s not quite true.

As a side-note, I’ve now realized one piece of magic that CocoaPods has. It does all this stuff for you somehow and that’s probably why there is a different configuration that it makes for each of mine. Should React Native be on CocoaPods? I don’t know.

Compile Flags

But I want the “test” build to run in “Debug” mode. Or maybe I need to debug the “Staging” build on the phone. In these cases, I’ve shown how are are compiling the app via the command line. This allows us to define extra, non-configuration variables. Therefore, we can use the regular ones like “Debug.”

There is a new Compiler class that pulls it all together. it basically uses xcodebuild to compile it and adds extra info like TEST_ENVIRONMENT=1, which the EnvironmentManager can then use to override the environment name.

It also uses the ios-deploy tool to put it on the phone if you ask it to do so. Try this out: npm run install:staging

Run Variables

When setting up schemes, I found that I could pass environment variables in the “Run” section of “Edit Scheme.” Then I’m using this to allow a “staging” name even though I’m in running the “Debug” configuration. This is then available as seen in the [[NSProcessInfo processInfo] environment] code above.

However, there is a fatal flaw. This is a run-time argument that is only used once. You lose that data if you launch the app again. It is, however, the best way that I’ve found to debug the “Staging” build in XCode.

Auto-Compile

So now there are lots of ways to launch and run this app, but I kept forgetting to bundle the new Javascript code when launching it from XCode onto the phone. There’s nothing worse than working on something and realizing 10 minutes later, the code on the phone is not the newest build.

The Compiler class does this automatically, but I looked for a way to automate the instructions in the AppDelegate. It wants you to run with localhost when in the simulator and run react-native bundle --minify when putting it on the phone.

So let’s automatically make those decisions based on the target runtime:

1
2
3
4
5
#if TARGET_IPHONE_SIMULATOR
  jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/index.ios.bundle?platform=ios"];
#else
  jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
#endif

This automatically causes it to use the bundle when running on the phone, but we still need to remember to compile it. So I added a “Run Script” build phase to do the bundle command:

1
2
3
4
if [ "${PLATFORM_NAME}" != "iphonesimulator" ]; then
    source ~/.nvm/nvm.sh
    cd ${PROJECT_DIR}/.. && react-native bundle --minify
fi

This assumes that you are using nvm.

Now it’s impossible to forget and everything is always based on the target. Nice.

Summary

I’ve updated the code of the Example App to have a few ways to build a React Native app with environment nuances. We’re mainly using the Configurations approach but the others have come in handy a few times. I hope that is helpful.

Copyright © 2017 Brian Leonard