During my time working with React Native, I have collaborated with many people from all around the world. While their skill sets and methodology varied greatly, many developers preferred using third-party libraries. Often there seemed to be hesitancy in creating solutions interacting with the native layer of the app. Even going so far as brute-forcing the implementation to work, spending both time and budget on a subpar solution due to being apprehensive about working with the native layer. Recently I fell into one such trap.
A client recently tasked me with creating a solution for Apple Pay, which conformed to the most popular React Native Apple Pay library since it was already implemented previously. I came to find out that the library was no longer supported!
I tried to vendor the library by branching off the most recent stable build. While the library worked, features for curbside or in-store pickup were not handled. Whether through a sunken cost fallacy or stubbornness, I kept trying to “make it work.” The amount of time and budget consumed to “make it work” made it apparent my approach needed to be reimagined.
There is an issue with the notion that using third-party libraries for everything because “why re-invent the wheel?” This continued for a week or so until I finally gave in and interacted with the native Swift layer. Within 2 days, the solution was complete and worked far better than the third-party library!
This tutorial will show you how to bridge the gap between native iOS via Swift and React Native to create much more streamlined and performant code in 4 steps.
Step 1:
Import the correct libraries into the bridging header of your React Native app.
#import <React/RCTViewManager.h> #import <React/RCTEventEmitter.h> #import <React/RCTUtils.h> #import <React/RCTEventDispatcher.h> #import "React/RCTBridgeModule.h" #import <React/RCTBridgeModule.h>
This will import the React Native libraries needed to bridge the gap between the two layers.
Step 2:
Create a module bridge for the library that you will be creating. Create a new file called TestModuleBridge.m ( or {ModuleName}Bridge ) and add the following code with any customization in function and file naming.
#import <Foundation/Foundation.h> #import "React/RCTBridgeModule.h" #import "React/RCTEventEmitter.h" @interface RCT_EXTERN_MODULE(TestModuleBridge, RCTEventEmitter) RCT_EXTERN_METHOD(performActionUsingData: (NSDictionary *) data onResolve: (RCTPromiseResolveBlock) resolve onReject: (RCTPromiseRejectBlock) reject) @end
This functions as a header file that enables these functions to be invoked from React Native.
Step 3:
Now that you have declared these functions, you must define them. Create a new file called TestModuleBridge.swift and add the methods declared in the .m file.
@objc(TestModuleBridge) class TestModuleBridge: NSObject { let viewController: UIViewController? var currentResolutionBlock: RCTPromiseResolveBlock? var currentRejectionBlock: RCTPromiseRejectBlock? override init() { viewController = RCTPresentedViewController() } @objc(performActionUsingData: onResolve: onReject:) func performActionUsingData(_ data: NSDictionary, onResolve resolve: @escaping RCTPromiseResolveBlock, onReject reject: @escaping RCTPromiseRejectBlock) -> Void { guard let id = data["id"] as? String, let name = data["name"] as? String, let phone = data["phone"] as? String else { reject("Insufficient parameters sent", nil, nil) return } currentResolutionBlock = resolve currentRejectionBlock = reject // TODO: Add code needed to set up and perform action // TODO: Add code/delegate methods needed and pass completion or // rejection block back to the app. } }
The goal of this file is to perform an action natively. Perhaps you need to interact directly with a framework such as PassKit or ARKit and bridging this behavior enables finer control over how the React Native layer interacts with Native Swift. After the native interaction is complete, defined delegate methods should be used to resolve the interaction on the React Native layer, often by passing data back.
Step 4:
Invoke the actions you will need to import the bridge and call the methods.
import { NativeModules, NativeEventEmitter } from 'react-native'; interface TestModuleBridge extends NativeEventEmitter { performActionUsingData(data: TestModuleBridgeInitType): Promise<unknown>; } const testModuleBridge: TestModuleBridge = NativeModules.TestModuleBridge; export const invokeBridgeFunction = async (passedData: PassedDataType) => { const data = { id: passedData.id, name: passedData.name, phone: passedData.phone, }; try { const response = await testModuleBridge.performActionUsingData(data); if (response) { // Do something with the response return response; } else { console.log('Error parsing response'); } } catch (error) { console.log(error); throw error; } };
This imports the native module, creates the data to pass to the module, then awaits a response after it has been invoked.
Creating a native module is not particularly difficult but gives noticeable benefits such as:
- More control over the interactions between the native Swift and React Native layer
- Protection against third-party libraries that may lose support or use deprecated code
- Offer personalized experiences without being limited to a third-party library author’s implementation
While writing custom native Swift modules is not meant to deter you from using third-party libraries, it is a good alternative in the event you cannot find a library to support your needs. The next time you find yourself stumped or encounter resistance using a third-party library, consider writing your own module. Don’t let the unknown feel daunting, even if you don’t know how to write native code. You may find that you not only completed the task faster, but you also learned new things along the way!
Perficient’s Mobile App Expertise
The Perficient Mobile Solutions team has extensive experience building mobile Apps. For more information about Perficient’s Mobile Solutions expertise, subscribe to our blog or contact our Mobile Solutions team today!