DEV Community

Ajmal Hasan
Ajmal Hasan

Posted on • Updated on

Mastering iOS Native Bridging in React Native

In recent years, Swift has become the preferred language for iOS development, including for native bridging in React Native. Swift offers modern syntax, better safety, and more powerful features compared to Objective-C.

Let’s explore how to create a native module in Swift for React Native, covering data passing, promises, callbacks, events, and UI components.


Setting Up a Basic Native Module in Swift

Step 1:

Open the iOS project in Xcode.
open ios/SwiftNativeModulesExample.xcworkspace

Step 2: Create a Swift File

  1. In Xcode, right-click on the SwiftNativeModulesExample directory, select New File..., choose Swift File, and name it NativeModule.

  2. Add Bridging Header:
    When prompted to create a bridging header, click “Create Bridging Header”. This file allows Swift and Objective-C code to interoperate.
    Ensure your NativeModule.swift file is created inside the SwiftNativeModulesExample target.

Step 3: Implement the Native Module in Swift:

//
//  NativeModule.swift
//  RN_NativeModules
//
//  Created by Ajmal Hasan on 06/07/24.
//

import Foundation
import React

// Declares the class as an Objective-C class and makes it accessible to React Native
@objc(NativeModule)
class NativeModule: NSObject {

  // Indicates whether this module should be initialized on the main queue
  // Set to true if setup on the main thread is required (e.g., when sending constants to iOS or making UI using UIKit in iOS)
  // Set to false to run this module on the main thread before any JS code executes
  @objc static func requiresMainQueueSetup() -> Bool {
      return true 
  }

  // A method that logs a message to the console
  @objc func logMessage(_ message: String) {
    NSLog("%@", message) // Logs the message to the Xcode console
  }

  // A method that adds two numbers and returns the result via a callback
  // Callbacks are used for handling asynchronous operations
  @objc func add(_ a: Int, b: Int, callback: RCTResponseSenderBlock) {
    let result = a + b
    callback([NSNull(), result]) // Calls the callback with the result
  }

  // A method that subtracts two numbers and returns the result via a promise
  // Promises provide a more readable and convenient way to handle asynchronous operations
  @objc func subtract(_ a: Int, b: Int, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
    if a >= b {
      resolve(a - b) // Resolves the promise with the result if `a` is greater than or equal to `b`
    } else {
      let error = NSError(domain: "SubtractError", code: 200, userInfo: nil)
      reject("subtract_error", "a is less than b", error) // Rejects the promise with an error if `a` is less than `b`
    }
  }

  // A method that sends an event to JavaScript
  // Events allow native modules to send data to JavaScript asynchronously, without waiting for a direct method call
  @objc public func myMethod(_ param: String) {
    print(param) // Prints the parameter to the console

    RNEventEmitter.emitter.sendEvent(withName: "onReady", body: [param]) // Sends an event to React Native with the name "onReady" and the parameter as the body
  }
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  1. @objc(NativeModule): This annotation makes the Swift class available to Objective-C, allowing React Native to call its methods.
  2. requiresMainQueueSetup: This static method indicates if the module requires initialization on the main thread. Returning true is necessary if the module needs to interact with UI components or send constants to iOS. Returning false means it can be initialized on a background thread.
  3. logMessage: A simple method to log a message to the Xcode console using NSLog.
  4. add: This method takes two integers, adds them, and returns the result via a callback. Callbacks are a common pattern in JavaScript to handle asynchronous operations.
  5. subtract: This method takes two integers, subtracts them, and returns the result via a promise. Promises provide a more readable and convenient way to handle asynchronous operations compared to callbacks.
  6. myMethod: This method sends an event to JavaScript using an event emitter. Events allow native modules to send data to JavaScript asynchronously, without waiting for a direct method call.

Step 4: This file defines the event emitter class in Swift.

//  RNEventEmitter.swift

import React

@objc(RNEventEmitter)
open class RNEventEmitter:RCTEventEmitter{

  public static var emitter: RCTEventEmitter! // global variable

  // constructor
  override init(){
    super.init()
    RNEventEmitter.emitter = self
  }

  open override func supportedEvents() -> [String] {
    ["onReady", "onPending", "onFailure"]  // etc.
  }
}

Enter fullscreen mode Exit fullscreen mode

Explanation:

  1. @objc(RNEventEmitter): This annotation makes the Swift class available to Objective-C, allowing React Native to call its methods.
    1. public static var emitter: A shared instance to access the emitter from other parts of the native module.
    2. init(): Initializes the emitter and sets the shared instance.
    3. supportedEvents: Overrides the method to define the supported events. This method should return an array of event names that can be sent to JavaScript.

Step 5: Register the Module:

Create a new Objective-C file to register the Swift module.

//
//  NativeModuleBridge.m
//  RN_NativeModules
//

#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>

// Interface declaration for the NativeModule to expose it to React Native
@interface RCT_EXTERN_MODULE(NativeModule, NSObject)

RCT_EXTERN_METHOD(logMessage:(NSString *)message)
RCT_EXTERN_METHOD(add:(NSInteger)a b:(NSInteger)b callback:(RCTResponseSenderBlock)callback)
RCT_EXTERN_METHOD(subtract:(NSInteger)a b:(NSInteger)b resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(myMethod:(NSString *)param)

@end

// Interface declaration for the RNEventEmitter to expose it to React Native
@interface RCT_EXTERN_MODULE(RNEventEmitter, RCTEventEmitter)

RCT_EXTERN_METHOD(supportedEvents)

@end
Enter fullscreen mode Exit fullscreen mode

Explanation:

  1. RCT_EXTERN_MODULE(RNEventEmitter, RCTEventEmitter): This macro declares the RNEventEmitter class to React Native.
  2. RCT_EXTERN_METHOD(supportedEvents): Exposes the supportedEvents method to React Native, making it accessible from JavaScript.

Step 6:

Using the Native Module in JavaScript

import {
  Button,
  NativeEventEmitter,
  NativeModules,
  StyleSheet,
  View,
} from 'react-native';
import React, {useEffect} from 'react';

const {NativeModule} = NativeModules;
const eventEmittedFromIOS = new NativeEventEmitter(NativeModules.RNEventEmitter);

const IOSNativeModules = () => {
  useEffect(() => {
    // 1. NativeModule.swift(also added bridging header by default)
    // 2. NativeModuleBridge.m

    // will print log in xcode after running from xcode
    NativeModule.logMessage('Hello from React Native!');

    // will print log in RN bundler/xcode
    NativeModule.add(3, 4, (error, result) => {
      if (error) {
        console.error(error);
      } else {
        console.log(result); // Logs 7
      }
    });

    // will print log in RN bundler/xcode
    async function subtractNumbers(a, b) {
      try {
        const result = await NativeModule.subtract(a, b);
        console.log(result); // Logs the result
      } catch (error) {
        console.error(error);
      }
    }
    subtractNumbers(10, 5);
  }, []);

  const sendDataToIOS = () => {
    // iOS Files Modified for NativeModules(sending data from RN to iOS):
    // 1. NativeModule.swift(also added bridging header by default)
    // 2. NativeModuleBridge.m
    NativeModule.myMethod('THIS IS SENT FROM RN TO IOS');
  };

  useEffect(() => {
    // iOS Files Modified for NativeModules(sending data from iOS to RN):
    // 1. RNEventEmitter.swift
    // 2. NativeModuleBridge.m
    // 3. NativeModule.swift
    const eventListener = eventEmittedFromIOS.addListener('onReady', string => {
      console.log('sendDataToIOS to IOS then from IOS to RN: ', string);
    });
    return () => eventListener.remove();
  }, []);

  return (
    <View style={styles.main}>
      <Button title="Send Data From RN To IOS" onPress={sendDataToIOS} />
    </View>
  );
};

export default IOSNativeModules;

const styles = StyleSheet.create({
  main: {flex: 1, justifyContent: 'center', alignItems: 'center'},
});

Enter fullscreen mode Exit fullscreen mode

ANDROID PART ->

Top comments (0)