During the last week or so I’ve been working on detox, our emerging graybox e2e testing framework (I’ll write about it separately when it’s ready). One of the challenges there is exposing to Javascript a native iOS library that has over a hundred API’s. If you’ve ever implemented a native wrapper in React Native, you know it’s a tedious process filled with boilerplate code.

There had to be a better way to access the full breadth of this native API from Javascript. I wasn’t about to wrap each API individually just so I can access it from the other side of the bridge.

If you’re unfamiliar with the bridge concept of React Native, see the high-level overview in my previous post.

Reflection to the rescue

Reflection in software engineering is the ability of a computer program to examine, introspect, and modify its own structure and behavior at runtime. It’s a powerful meta programming tool that allows our code to manipulate the code being run. Luckily, both Objective-C in iOS and Java in Android support reflection to a usable extent.

This became our plan:

Our Javascript code will specify which native method should be invoked by specifying it as a simple string. The arguments for this method will be provided as an array (assuming they’re serializable). Since this entire specification is now serializable, we can send it over the React Native bridge to the native side. Our helper library in the native side will parse the specification and use reflection in runtime to locate the method by string on the relevant class. If the method exists, we’ll use reflection to invoke it. The idea behind this dynamic approach is that our native helper doesn’t need to know in advance which methods we’re planning to execute. Assuming the native return value is serializable, we’ll send it back over the React Native bridge and consume it in Javascript.

Let’s use this technique on a real life example

If you’ve ever used React Native’s ScrollView, you may have noticed that it doesn’t have a getScrollOffset method. The only way to get the scroll offset is to subscribe to scroll update events and remember the value — and that’s hackish (and generates unnecessary traffic over the bridge).

React Native’s ScrollView is a wrapper over the native UIScrollView — which naturally does contain API to read the content offset. Let’s use our new technique to execute this native getter method directly from Javascript:

The first line implements step 1 in our plan — the Javascript code specifies (as a string) that we want to use the native contentOffset method.

The second line implements step 2 in our plan — it takes the specification and sends it over the bridge to be executed. Notice how it returns a promise with the result since the code will be executed asynchronously in native.

But what happens when things aren’t serializable?

The scroll offset is a simple serializable object — just X and Y. Which means it can easily pass over the bridge as our plan requires.

This might not always be the case. When I tried playing with the scroll offset example, I originally planned scrollComponent to be our React ScrollView component instance. It turns out that the React ScrollView does not extend UIScrollView directly, it contains it as a child available through a native getter named scrollView.

Not a problem, right? Let’s just make another prior native execution to get a reference to the native UIScrollView from our React component:

Unfortunately, this won’t work. The problem is that scrollView is a complex object — it’s a reference to the native UIScrollView and it isn’t serializable as our approach mandates.

To work around this issue, we’ll do a very cool trick. Instead of sending over a single command to execute in the native side, let’s make our technique more powerful by allowing to send over multiple commands that depend on each other. Since all commands will be executed together in the native side, they will be able to pass complex objects between them! Since these complex objects won’t need to go back over the bridge to JS, they won’t have to be serializable.

Our new approach delivers the following implementation that will work:

As you can see, we now have a single execution that involves two different calls in the native side. The result of the first call isn’t serialized as a promise back to JS, it’s simply given as part of our specification to the second call.