React Native makes it easier than ever for web developers to cross over into mobile development. With its familiar JSX components and JavaScript ecosystem, it has made cross-platform development more accessible. However, the native build pipeline can still pose significant challenges, especially for those new to mobile development. The intricacies of Xcode, Cocoapods, and other native tools often create obstacles and slow down the workflow.
At Monarch, we've addressed this issue by eliminating the need for developers to build the native app from scratch. Our Continuous Integration system is configured to build a debug version of the native binary, which developers can then download and install on their simulators. Since most changes are made in the JavaScript code, developers only need to run the Metro bundler for the simulator to connect with. This approach slashed our iOS cold start time from over 20 minutes to just a matter of seconds and significantly reduced local build errors.
The Problem with Traditional Native Build Pipelines
There are two parts to building a typical React Native app: the native binary and javascript bundle. When running the app locally on the simulator, developers must first build & install the native binary using Xcode. Then a Metro bundler server is run continuously to provide the javascript bundle to the native binary and rebuild as changes are made.
Building the native binary is time consuming and error-prone, especially for developers well-versed in the javascript ecosystem but new to mobile development. For iOS, this means dealing with Xcode, managing dependencies with Cocoapods, and navigating the complexities of code signing and provisioning profiles. Building the Monarch app from scratch can take well over 20 minutes, and when it fails the error messages are cryptic and difficult to track down fixes for. As our team grew and we onboarded more engineers, we discovered that getting the app to build from scratch on a new engineer's machine often required several hours of collaborative troubleshooting.
Our Solution: Pre-Built iOS Binaries
To streamline our development process at Monarch, we implemented a solution using pre-built iOS binaries. We've shifted the task of building the native binary from individual developers to a CI server, effectively removing this burden from our team.
Now, when developers want to start working on iOS, they simply run yarn ios
and within seconds they’ll have a simulator running a debug version of the app. This is made possible by a custom ios
script which downloads the pre-built binary, installs it on the iOS simulator, and finally starts Metro bundler.
Benefits of Using Pre-Built iOS Binaries
Time Savings: By downloading a pre-built binary, developers can avoid the lengthy build times associated with Xcode. This allows them to start the app on the simulator in a matter of seconds instead of minutes or hours.
Reduced Complexity: Developers no longer need to worry about managing Cocoapods dependencies, dealing with Xcode configuration issues, or resolving build errors. This makes the mobile development process more accessible for engineers not as familiar with the mobile build pipeline.
Focus on Javascript: Since most of the development work in React Native involves writing JavaScript, this approach allows developers to focus on their JavaScript code without being distracted by native build issues.
Second-Order Effects: Developers on our team are now more likely to test mobile changes and quick-fix mobile-specific bugs than they were before.
Technical Setup
To implement this new workflow, several key components were necessary:
Continuous Integration: Our CI system is configured to automatically build a debug version of the native iOS app.
Upload and Distribute Binaries: The
ios
script then downloads the generated binary which is stored in a central location.Install on Simulator: The script also .automatically installs the downloaded binary on the iOS simulator using
xcrun simctl
.Metro Bundler: The Metro Bundler serves the JavaScript bundle to the app running in debug mode on the simulator.
Step 1: Continuous Integration Setup
We already had CI setup with Fastlane to build our production app, so this was just a matter of adding another lane to build with the “Debug” configuration instead of “Release”.
1
2
3
4
5
6
7
8
9
10
# Adding a lane in Fastfile
desc "Build simulator binary for local development"
lane :build_simulator_debug do
xcbuild(
workspace: "./ios/mobile.xcworkspace",
scheme: "Debug",
configuration: "Debug",
xcargs: "-sdk iphonesimulator SYMROOT='/var/tmp/'"
)
end
Step 2: Upload and Distribute Binaries
The built binary is then uploaded to a central location for team members to download. We opted for a separate git repository that houses only the zipped binary. While there are various ways to store this file, this method was most convenient for us. Our team members already have their git SSH keys set up, eliminating the need for additional authentication when downloading. We perform a force push each time we upload a new binary, preventing old binaries from cluttering the git history and keeping the repository size manageable.
You can see sample code for this script here.
Step 3: Install on Simulator
The iOS simulator has a convenient feature that allows you to install an app by dragging a .ipa
archive onto its window, mimicking the process of building and running from Xcode. This was the core discovery that led us to the pre-built binary idea in the first place. Instead of having to do this manually, we've automated this in our yarn ios
script. After downloading the pre-built binary, it employs Xcode command-line tools—specifically xcrun simctl
—to launch the appropriate simulator instance and install the binary automatically.
You can see sample code for this here.
Step 4: Run Metro Bundler
After downloading and installing the binary on the iOS simulator, we simply start the Metro bundler server. The debug-configured binary automatically searches for and connects to this server upon startup, just as it would normally. As developers make changes to the JavaScript code, the app live-reloads seamlessly, preserving the familiar development experience.
Conclusion
By leveraging pre-built iOS binaries at Monarch, we’ve reduced our iOS development cold start time from over 20 minutes to a few seconds. This approach not only saves time and reduces complexity, but also enhances productivity and allows our developers to focus on what matters most: building great apps.
You can find some of scripts we used for this project here.