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

Marc Laberge
SSENSE-TECH
Published in
11 min readOct 21, 2022

À Table!

In part I of this series, we discussed the basics of how to cook a React Native Bridge with Swift. By now, you should know how to expose Swift classes, modules, static data, and methods to TypeScript. In the last example of part I, we learned how to use different kinds of callbacks and we noticed that in some cases, we may have preferred to use something like Promises.

The goal of this article is to go a bit further into some capabilities, so I’ll continue to refer you to another article if you have questions on more basic things. When I mention topic numbers, I’ll refer to that article.

Don’t forget that you can access the entire open source project used to write this article here.

Main Course

Was exposing a method as a Promise (topic 6 of the referred article) too easy? If so, let’s go further! Not only will we play with Promises, but we will order the best poutine ever (yes, I’m a Quebecer) with async/await and a custom complex object in param. “Wait? But I thought you told us we couldn’t”. Well you probably already saw it coming, we will pass a JSON object in param. In most cases, I would recommend to still pass all params individually if you can, but if you want to send a bigger payload, that’s one option you now have on the table.

I think we should order our poutine if we don’t want to wait too long. For this part, let’s open the Main Course folder on both the iOS and React Native sides. I have specially created a poutine.type.ts just for you. Basically, it’s just an interface that contains a bunch of enum ingredients. I have also created a PoutineFactory with my special pre-built favorite poutine. Wow, I’m so famous that I made it to the menu of my own imaginary restaurant.

Ok, let’s have a look at the orderMainCourse on the React Native menu that the waitress just gave us.

First of all, we selected our poutine from the factory, but we had to build an extra method order(poutine: Poutine) that the waitress will use to stringify our Poutine object into a JSON string.

So that’s how?! Yep, no one said we couldn’t send an object in a string costume.

Well, let’s see how it goes on the iOS side. Again, nothing crazy on the .m file, the sendMainCourseOrder takes an NSString in param.

In the SwiftMainCourse.Swift file, we use the JSONDecoder to decode that string param back into an actual object. Yes, we had to redeclare a Codable Poutine struct in Swift to do so, but I have to say I’m impressed by how compact it is. And, since my Swift Poutine is Codable, I’ll also be able to use JSONEncoder to transform my poutine into a string JSON if I want to (not that I really need it here).

So what happens once all the JSON magic has taken place from React Native to Swift? That’s where Promises come to the plate. Well, maybe not to the plate just yet, but soon enough since I still have to explain to you how Promises work before rejecting or resolving you.

Let’s compare the getLastAppetizer form our “Starters” chapter to sendMainCourseOrder. Starting with the .m files:

The semantics are pretty similar to be honest. Let’s talk about the differences:

Success or Failure callbacks are represented by RCTResponseSenderBlock (or RCTResponseErrorBlock for failure case only). They are function blocks.

You are probably familiar with Promises and I assume you already know how to expose a basic method as a Promise (topic 6 of the referred article), but as a quick reminder you can either resolve or reject. You can identify your resolver with RCTPromiseResolveBlock and rejecter with RCTPromiseRejectBlock.

I understand that if you have no prior experience with Promises, you are probably confused about which to use in which cases. It’s ok, this article brings people with different backgrounds together. I might teach you a thing or two even if you are a master.

First, note that as mentioned in the “Starters” chapter, you can use RCTResponseSenderBlock and RCTResponseErrorBlock individually. In other words, one doesn’t depend on the other.For a Promise, you have no choice but to use both RCTPromiseResolveBlock and RCTPromiseRejectBlock together all the time. This makes sense to me, but if you’re not convinced, try it yourself and you’ll see that the compiler is explaining it in a less friendly way.

Basically, whenever you need to notify both success and failure, I would use Promises. If you only need success (or failure), use Callbacks. Again, this is just a general statement for beginners, but I thought it was worth mentioning.

Is that poutine coming already? You probably noticed that in this example we have wrapped our call into a try…catch and that we are using await (don’t forget to spot the async keyword). Basically we are waiting for a Swift cook Promise response before continuing and we are ready to handle any case coming back to us. Damn! We got rejected because there was no more steak available. We let the React Native waitress immediately say that we want the same exact poutine without the steak and reorder again.

While we are waiting, let’s talk about RCTPromiseRejectBlock:

typedef void (^RCTPromiseRejectBlock)(NSString *code, NSString *message, NSError *error);

If we compare it to callback failure, you notice that this one takes a string error code and a string message in addition to an NSError. You’ll tell me that it’s already possible to specify a numerical error code and a string domain in NSError. That’s true, but it does not look great. In my example, I’m able to send the “meats_steak_issue” error code and “We don’t have steak in the kitchen” as a message. I think that it’s a lot easier and more intuitive to read compared to looking at an error.domain as a message. Payload will also be smaller because I have set the error to nil.

Our long awaited poutine is finally arriving! The Swift Cook has resolved with a positive string message. Let’s enjoy the smell a little bit and talk about RCTPromiseResolveBlock:

typedef void (^RCTPromiseResolveBlock)(id result);

Depending on your background, you are probably asking yourself what the heck is that super id type right? Well, it’s just a super confusing way for Objective-C to say Any in Swift. You can read more about it in this Apple documentation: https://developer.apple.com/Swift/blog/?id=39.

In my example, I’m only resolving with a simple string, but just know that you could technically send back any kind of object to React Native, as long as it is a supported type. For a full list of the supported types please check here.

Enjoy your poutine while you can, because something sweet is coming for dessert.

Dessert

Hope you are not too full, because the dessert is delicious. But I would understand if you want to stop here. I myself don’t order dessert often and you probably already have all the iOS bridge knowledge you need. However, if you want to indulge, let’s look at how to expose an Event Emitter (topic 7 of the referred article).

Let’s jump into Dessert files now!

Have you noticed how most examples on the Internet of how Swift classes are used to build bridges are NSObject? How boring is that? Not to mention the fact that we can’t override the init of the bridge outside the iOS (or from React Native). You need to set properties from bridge methods and make sure to always call it first if you have other methods that depend on those values.

What you can do instead is override the initialization only on the Swift side. That’s what brings me to introduce you to Emitters, this allows you to send events from Swift to React Native. Wait, what about the init story? In my example, instead of using NSObject for my class, I’m using RCTEventEmitter (don’t forget to import React and React/RCTEventEmitter.h). I have also added a static optional Emitter: RCTEventEmitter. This is why I need to override the init SwiftDessert.emitter = self. Again, I’m showing you what you can do, not necessarily what you should do. I’ve come across some examples where you can create an individual EventEmitter class that manages all events from there. In my case, I wanted to remove as much complexity as possible (multiple modules, initializer issues, …). Let’s continue.

Always on the Swift file, you need to override supportedEvents that essentially returns an array (of string) of all the events you could send. Once you have that done, you are all set to send events from Swift.

Now, in the Dessert React Native file, notice how I’m not only setting a const to access the NativeModule. I have to create a new NativeEventEmitter with it. After that I’ll be able to add and remove listeners to my events.

At this point, you are probably wondering in what situation you would even need to use Emitters for a bridge. You’ll be surprised that I had to use this for a real specific case. We will get there soon, but for now let’s stick to our dessert before the restaurant closes.

If we look at the React Native side, at orderDessert, I chose to add the Emitter listener of “swift_cook_has_finished” event first. Another possible param of addListener is the listener function and there’s also another param that is the context of the listener, but I’m not using it in my example. I like to compare the listener function (second param) to a kind of callback, because his body will be executed at the event reception.

If you have not noticed yet, I have created swiftCookIsStillThere: boolean and swiftCookLastMessage: string = “” variables in dessert. So in our listener function, I’m simply setting those variable values when we receive this particular event. Finally, I’m removing the listener since we will no longer need to listen in this event anymore.

Let me give you some more context on what we actually want to accomplish here. We want to order a dessert. And, spoiler alert, we only have one dessert that you can’t order more than once (the client doesn’t know this yet) even if you want to.

Let’s have a look at SwiftDessert.sendDessertOrder:

The first param is to specify if this is hisFirstDessert ordered. We will set this value to true the first time the client orders the dessert. We already know that they’ll try to order it again and again, because it’s so tasty, but in this case we will send hisFirstDessert as false. We are tracking this with a dessertCount: number variable on the React Native side. For the rest, you already know, we will be using Promises. Surprise! You don’t need to specify anything for the Emitter part at the method level since you already specified that the module/class was RCTEventEmitter.

Ok, so now that you have a bit more context, if we call orderDessert one time, we will get back the special WW MELTING CHOCOLATE SPHERE DESSERT (Promise resolved), because it was our first dessert order attempt.

On the second call of orderDessert, you can observe that after a short delay, on the Swift file, our Emitter is sending the event “swift_cook_has_finished” with body “Thank you, hope to see you again soon!”. Remember that this will not show any message to our client yet, it will only set our swiftCookIsStillThere and swiftCookLastMessage variable in the React Native code.

Finally, since it was the second dessert attempt, we were rejected with “dessert_overflow” error code and “We only allow one dessert per person.”.

Our perspective right now is that the Emitter was not necessary and we could have accomplished what we wanted only with the Promise. Wait! Let’s be polite and compliment the Chef for this delicious meal. To do so, I have created another method called giveFeedback that will be responsible for sharing client messages that waitresses will have identified as positive or negative beforehand. But since the restaurant will officially close soon, it’s possible that the Swift Cook has already left. Let’s analyze two scenarios:

  1. You have ordered the dessert only once and then you try to give the feedback. In this case the Emitter will never send the “Swift_cook_has_finished” event, this means that the React Native waitress will try to give the feedback to the Swift cook. If the client message is positive, then the Cook will resolve with a thank you message. If it’s negative, he will reject it with “bad_feedback” error code and “We are sorry, we will do better next time” message.
  2. You have attempted to order dessert twice, so the Emitter has sent the “Swift_cook_has_finished” event with the message already. When you try to give the feedback, the waitress will know that the Swift Cook has already left from the received event from the Emitter and will simply not try to give the feedback. She’ll be able to let you know that the chef has left and share the message he transmitted with the event.

I see you thinking of alternatives right now and it’s ok. I was trying to showcase an example that fits with my fictive restaurant problem using Emitters.

Now that we went over all my examples, I promised earlier to share a case where I had to use RCTEventEmitter. Actually, one thing I may have forgotten to mention is that you can’t resolve or reject more than once in the same method call, which kind of makes sense in general. But in case you need to resolve more than once, that’s where sending events becomes useful.

Let’s give you a real example, I had to implement a bridge on Apple Pay using one of our payment processors SDK. If you are not familiar with Apple Pay iOS implementation, you basically have to conform your class to PKPaymentAuthorizationControllerDelegate in general. Some SDK will provide a different delegate that under the hood also conforms to that delegate.

When users try to pay with Apple Pay, a native sheet is shown. After the user authentication with TouchID or FaceId, a token is created and didAuthorizePayment is called inside the delegate. Long story short, you’ll probably want to share that token from Swift to React Native and then call your authorized endpoint.

To dismiss the Apple Pay sheet, you normally have to call the completion that comes with didAuthorizePayment delegate method. But remember, I told you that some SDK override the delegate with their own delegate. For example, one of them was required to call the completion with a certain id that would only come back from our authorized call in order to dismiss the Apple sheet automatically. Ok, so we need to do an extra hand shake, that’s fine, we can simply create another method that will be responsible to call the completion method when necessary. It works!

But of course, there’s an edge case you might not have thought of. If the user has authorized Apple Pay payment, but the authorized<>completion handshake did not happen yet, it’s possible that the user will dismiss the sheet by himself. In that case, the completion will no longer exist and if you call your handshake method with an await, you’ll wait for a long time. Here’s the problem: From the delegate, I know when my user attempts to dismiss the sheet himself, but we already resolved and we can’t resolve again, so we need a way to notify React Native that it does not need to call the handshake at all. That’s when the Emitter comes in handy

Check Please!

As you can see there is a lot to unpack, but I hope this helped you understand what goes into creating your first bridge. I gave you the big picture.

It’s a long article that completes a previous article and there’s so much more that could be explored. This process makes me want to learn and teach you the equivalent for Android using Java and Kotlin. Would you be interested?

Editorial reviews by Catherine Heim & Mario Bittencourt

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

--

--