How to Cook a React Native Bridge with Swift [PART 1]

Why React Native?

Marc Laberge
SSENSE-TECH
8 min readSep 16, 2022

--

In 2019, SSENSE released a native iOS app offering our customers an intuitive and highly personalized way to browse our extensive product catalog. We also introduced some features using SwiftUI (see our other article about it). Last year, we released our first version of the Android app, built using React Native. You may be wondering why we did not choose to develop it using Kotlin with Jetpack Compose instead, since we already had a pretty good native iOS app built exclusively with Swift. Well, the answer is simple: using React Native allowed us to maintain only one code base, instead of two, and to have a common tech stack among the mobile developers for both platforms.

To enable this integration we had to build bridges, which is basically a way to communicate back and forth between React Native and the native part of the application. For Android, most of the time, the native part will be in Java. For iOS, native will be Objective-C or Swift.

Now that you have more context, I want to share with you a couple tricks to better build React Native bridges for iOS using Swift. To guide us in our journey, I have built a fictional and open source project that you can access here.

The goal of this article is to dig a bit deeper into certain capabilities, so I’ll refer you to this article if you have questions on more basic aspects. When I mention topic numbers, I’ll refer to that article.

Let’s cook!

Reservation

Once you understand how to set up, expose Swift class to JS, and expose static Swift data, covered in topics 1 to 3 of the referenced article, we can begin with my “Reservation” example, planning and eating in my restaurant.

First, the constants. If by any chance you think it’s better that some constants live only on the iOS project, you might want to instantiate them directly in your Swift file. Know that it’s also possible to share some of those constants from Swift to React Native by using overriding constantsToExport().

In this method, you can return a dictionary of “Any” [AnyHashable : Any], but be careful because technically it’s returning an NSDictionary (Cocoa class) which is a bit different from the Dictionary (Swift struct) you might be more familiar with. The main difference is that Dictionary has specific types, while NSDictionary does not.

To come back to our example, I’m declaring 2 string constants restaurantName and phoneNumber that are returned in a dictionary from Swift.

In the tsx file, I’m able to access them by calling getConstants() on my native module. I can then use and show those values in the React Native component if I need to.

Next, let’s make a reservation at my restaurant and take it further with topic 4 covered in the referenced article. For this example, I’m exposing a simple Swift method that I named makeReservation.

Keep in mind that you can only pass specific argument types when you build a bridge. One of the parameters I’m passing is numberOfPeople which should be of type integer here, but in typescript it’s a number, in Objective-C it’s an NSNumber, and in Swift I cast it as a Double. It’s important to know the differences between all these allowed types and how to cast them correctly from one language to another. Most references I found on the subject stick to simple types, that’s why with my examples I’m showing you some tricks on how to pass more complex argument types.

Let’s attack one example now, in makeReservation, the second parameter is a date. If you look at the argument type list from documentation, you’ll see that it’s not possible to do it as is. Here’s my trick: I’m creating a Typescript Date that I transform into a string.

In the .m file, I consider it as an NSString pointer and in the Swift file it’s simply a String. In Swift, I’m able to use ISO8601DateFormatter from Foundation library to transform that string into a beautiful Swift Date in 2 lines of code. Like it or not, this is a creative way to send a date from one side to another.

Now, this example is not super useful since it’s just a one way communication (React Native > Swift). I hear you, but hold on, we are just laying the groundwork before getting started for real.

Starters

Now that you are familiar with the basics, let’s order our appetizers.

In the previous example I showed you that you were able to return a dictionary from Swift to React Native, well the reverse is also possible. If you look at the orderingStarters method in my .tsx file, I’m creating a simple key(string) with value(string) dictionary and passing it as a NSDictionary to Swift.

Speaking of dictionary, I already gave you a taste of NSDictionary versus Dictionary and I want to continue on this, so I decided to create extensions to NSDictionary to return an actual [String: String] Swift Dictionary (See NSDictionary+Dictionary.Swift file).

I know there is quite a bit of work and duplication involved to simply transform to the correct type, but now you know you can do it. This is far from being a finality and I still have other tricks up my sleeve. Technically, Dictionary is the most complex type you pass back and forth between React Native and Swift. Really? We will double check that soon.

I want to quickly touch on syntax here because not everybody will understand it. If you’ve done a bit of Objective-C, there’s a nice wink here. You probably noticed in the .m files that when you create RCT_EXTERN_METHOD, it looks like this:

and translate to something like this in the Swift file:

If you are not familiar with Specifying Argument Labels, I encourage you to read: https://docs.Swift.org/Swift-book/LanguageGuide/Functions.html.

Basically, just like with Objective-C, the first attribute doesn’t have a label and in Swift you need to stamp it as _. For the subsequent attribute, you need to specify a label. It’s also true for callbacks and promise blocks.

Speaking of which, I think that we are ready to order our first appetizer using callbacks. I have to say that topic 5 on how to expose a method with a callback in the referenced article is pretty complete, but I have to show you some examples here. Now that we have ordered appetizers and a Swift cook is preparing it, the React Native waitress wants to make sure to not send appetizers too fast or too slow, so we have split them into individual methods.

In the Starters.tsx file, if you look at getFirstAppetizer, you’ll notice that we only pass a callback in the native module method that will basically let us know when the first appetizer is coming.

Let’s have a look at SwiftStarters.m. Notice that the first and only argument in this case is a pointer of type RCTResponseSenderBlock that simply represents a callback.

Now, in the associated Swift file, there’s nothing really special except the @escaping keyword. In most cases, you won’t need that keyword, but in my example I’m wrapping the callback in DispatchQueue.main.asyncAfter, so I need it. Another case where it would be needed is if you want to use the same callback in a different method. We will get to an example later in this article. The reason why I decided to wrap the callback here is to simulate a delay while Swift cooks the appetizer calling React Native callback.

Do you want to take a break before the waitress asks for the second appetizer?

We may have waited too long because it looks like we expect a failure callback by quickly looking at getSecondAppetizer.

While being pretty similar to our first appetizer example, you can see that instead of using RCTResponseSenderBlock, we use RCTResponseErrorBlock.

Note that you could have also used RCTResponseSenderBlock as a failure function, but I wanted to show you a case where we use it. Let’s talk about the differences between the two.

typedef void (^RCTResponseSenderBlock)(NSArray *response);typedef void (^RCTResponseErrorBlock)(NSError *error);

Basically, one needs to return an array and the other an error, well an NSError. I recommend reading more about this specific object if you don’t know enough about it. https://developer.apple.com/documentation/foundation/nserror

In my example, I’m creating an NSError: I use the domain parameter to share my error message, the error code parameter as 86 (which is a real cook code when a meal is not available ^^), and I set userInfo to nil, but just know it’s a dictionary of type [String : Any] that you could use to send more info.

After a little forced delay, the Swift is calling back React Native with the sad news that the second appetizer ordered is no longer available. We are only showing the “domain” message, because the client doesn’t need to know it’s an 86 kitchen problem.

Let’s test them for our last appetizer. In getLastAppetizer, we will send one boolean attribute, one success callback function, and one failure callback.

First parameter is if the waitressWantToOfferItForFree because she feels bad about the second appetizer issue.

Chef will answer with a success or failure response which is represented by the last two callbacks.

Nothing really special in the .m file here.

In the Swift file though, I had some fun…

The Swift cook will randomly choose if he wants to give the last appetizer for free or not. If both waitress and chef agree, the customer will get it for free. If only the waitress is ok with it, we will offer a 50% discount. Otherwise, we callback with a failure. In React Native cases, we also handle error cases, but I have the feeling that we will at least get 50% off in our example. Try it yourself and tell me if I was right. Only 1/2 is random, but we always get something ;-).

Before the Main Course

Wow, starters were good, and I have already covered a lot of concepts in our journey towards our native bridge. But I still think that there are better ways to do certain things… If only we could use Promises instead of two individual callbacks.

In part II of this article I will continue with our project, exploring the usage of async/await with a custom complex object as the parameter and using emitters to send events from Swift to React Native.

Editorial reviews by Catherine Heim & Mario Bittencourt

Want to work with us? Click here to see all open positions at SSENSE!

--

--