In the last few days I was working in pair programming with my colleague Mariano Patafio on some new features for our React Native mobile apps. Mariano is a senior iOS and Android developer and a true ” Apple fag ” (like me ). At some point during our pair session we wanted to test the app on a real iOS device. The app we were working on was an existing iOS app in which we added some React Native views. If you follow the instructions contained in the React Native documentation about integrating it in an existing app, you will discover that with that setup you will not be able to run your app on a real device from Xcode. It will work just in the simulator.
I can’t believe that
It should be possible with the right setup to build, run and debug your React Native app from Xcode. I’m going to demonstrate it using the React Native example app that you can find in this github repo.
The Sample App
The app is very simple: it contains a main screen with 2 buttons that let the user open two different React Native
views. Let’s see the original implementation of the app above, where we implemented a
ReactNativeBridgeDelegate
that returns the localhost url of the index.bundle
that contains our React Native code.
class ReactNativeBridge {
let bridge: RCTBridge
init() {
bridge = RCTBridge(delegate: ReactNativeBridgeDelegate(), launchOptions: nil)
}
}
class ReactNativeBridgeDelegate: NSObject, RCTBridgeDelegate {
func sourceURL(for bridge: RCTBridge!) -> URL! {
return URL(string: "http://localhost:8081/index.bundle?platform=ios")
}
}
If we try to build this app on an iPhone, and we open one of the React Native screen we will receive the following error. This is a consequence of the fact that we are trying to access localhost from the iPhone, and our React Native node server is running on the MacBook Pro where we are building the app.
React Native Xcode Builder
How can we build on a real device? First of all we need to add a new build phase to our project that let us run the
React Native Xcode Bundler
before the real build. The React Native Xcode Bundler
is a shell script with name
react-native-xcode.sh
that you can find inside your react native npm package under <you app root folder .>/node_modules/react-native/scripts/
. This script must take as input our React Native index.js
.
Now we can change our ReactNativeBridgeDelegate
implementation. Instead of returning an hard coded url, we use the
RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index", fallbackResource: nil)
method. We need to
pass "index"
as bundle root parameter (the name of the main js file).
class ReactNativeBridge {
let bridge: RCTBridge
init() {
bridge = RCTBridge(delegate: ReactNativeBridgeDelegate(), launchOptions: nil)
}
}
class ReactNativeBridgeDelegate: NSObject, RCTBridgeDelegate {
func sourceURL(for bridge: RCTBridge!) -> URL! {
- return URL(string: "http://localhost:8081/index.bundle?platform=ios")
+ return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index.bundle", fallbackResource: nil)
}
}
Now we can try to build an run again the app on a real device. As you can see now everything works as expected.
What’s happening?
What’s happening under the hood? Which kind of “magic” are we using here ? If we start to debug from the call
to RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index", fallbackResource: nil)
and we go
inside the React Native source code at some point we will see a call to a method named guessPackagerHost
. In this
method there’s a piece of code that tries to open and read the content of a file named ip.txt
(this file is
supposed to be in the main bundle of the app). The string returned by this method is used as hostname in the url used
by React Native to call the packager we are running on our Mac.
Who did create this ip.txt
file? Previously we added the execution of the React Native Bundler
script as build
phase. If we look at the source code of this script you will find the following piece of code:
What?!?!?!?!?!? This piece of code basically creates a file named ip.txt
that contains the IP
address of your computer, extracted using an ifconfig
command, concatenated with the domain xip.io
. So the file
will contain a string like the following one: <your computer IP address>.xip.io
. This is the string returned by
the guessPackagerHost
method. In the screenshot below you can find the source code of this method and the string
that it returns.
What is the xip.io
string added after the IP address? xip.io is a public free DNS server
created at basecamp. Below you can find a quote from the homepage of the service:
What is xip.io? xip.io is a magic domain name that provides wildcard DNS for any IP address. Say your LAN IP address is 10.0.0.1. Using xip.io,
10.0.0.1.xip.io resolves to 10.0.0.1 www.10.0.0.1.xip.io resolves to 10.0.0.1
mysite.10.0.0.1.xip.io resolves to 10.0.0.1 foo.bar.10.0.0.1.xip.io resolves to 10.0.0.1
…and so on. You can use these domains to access virtual hosts on your development web server from devices on your local network, like iPads, iPhones, and other computers. No configuration required!
How does it work? xip.io runs a custom DNS server on the public Internet. When your computer looks up a xip.io domain, the xip.io DNS server extracts the IP address from the domain and sends it back in the response.
This basically means that xip.io is a domain name we can use to access our local packager environment on our Mac from
our iPhone and iPad, as long as the devices are connected to the same network.
That’s all, and as you can see everything works “like magic” .