1. Setup

Create a new project

If you already have a React Native project, you can skip this step.

If you don’t have a project yet, you should create one. Please follow the official Getting Started guide of Building Projects with Native Code (not the Quick Start).

Throughout this guide, you will build a Counter App, so the first thing you need to do is to create a new project:

react-native init CounterApp

cd CounterApp

react-native run-ios

Next, you need to open ios/CounterApp.xcodeproj in Xcode.

Create a Swift file

From Xcode, just go to:

File → New → File… (or CMD+N )

) Select Swift File

Name your file Counter

In the Group dropdown, make sure to select the group CounterApp, not the project itself.

Make sure to select the CounterApp Group

Configure the Objective-C Bridging Header

After you create the Swift file, you should be prompted to choose if you want to configure an Objective-C Bridging Header. Select “Create Bridging Header”.

This file is usually named YourProject-Bridging-Header.h . Don’t change this name manually, because Xcode configures the project with this exact filename, as you can see in the Build Settings tab:

Note: there is only one Bridging Header per project, so once you have configured it, you won’t be prompted to do it again.

The added files should appear inside the CounterApp Group, within the project

Let’s add one of the headers we will need right away:

// CounterApp-Bridging-Header.h #import "React/RCTBridgeModule.h"

Declare a Swift class

To begin with, let’s just create an empty class called Counter. It should be inherited from NSObject so that it can be exposed to Obj-C:

// Counter.swift import Foundation @objc(Counter)

class Counter: NSObject {

}

2. How to expose a Swift class to JS

Next, you have to expose your Swift class to React Native. To do this, you need to use some Obj-C Macros available in React Native.

To use these Obj-C Macros, you need a new Obj-C file:

File → New → File… (or CMD+N )

) Select Objective-C File

Name your file Counter

// Counter.m #import "React/RCTBridgeModule.h" @interface RCT_EXTERN_MODULE(Counter, NSObject)

@end

You also have to import RCTBridgeModule , so that you can use the Macros to bridge the native code.

You’ll use the RCT_EXTERN_MODULE Macro to expose your Counter class to JS:

the first argument is the name of your Swift class;

the second is its superclass;

Rename your exposed module (optional)

In case you want to expose your module under a different name, you should use the RCT_EXTERN_REMAP_MODULE Macro instead:

the first argument is the name exposed to JS

the second argument is the Swift class

the third is its superclass

For instance, if you want to expose your module as RNCounter :

// Counter.m #import "React/RCTBridgeModule.h" @interface RCT_EXTERN_REMAP_MODULE(RNCounter, Counter, NSObject)

@end

Important: There is another very similar Macro, called RCT_EXPORT_MODULE , but that one is used with Objective-C code. We will use the one with EXTERN .

Access your module from JS

Everything should be set up on the native side. Now let’s move on to the JavaScript side.

In your App.js file, import NativeModules from react-native and let’s see if you can access your Counter module:

// App.js import { NativeModules } from 'react-native' console.log(NativeModules.Counter) // or NativeModules.RNCounter, if you have renamed it

Any module exposed to React Native should be available as a property on the NativeModules object.

But if you Debug JS Remotely, you’ll get … undefined .

Apparently, React Native doesn’t expose native classes if you don’t expose some of their methods, as well. So let’s do that.

3. How to expose static Swift data

The most simple thing you can expose is static data. All you need to do is implement a special method, called constantsToExport , that returns a dictionary:

// Counter.swift import Foundation @objc(Counter)

class Counter: NSObject { @objc

func constantsToExport() -> [AnyHashable : Any]! {

return ["initialCount": 0]

} }

Important: we need to use the @objc directive on each method / property that needs to be called or accessed from Obj-C.

Now, if you re-run the app, you should see the object in the JS console:

You can return any kind of data, but keep in mind that this data is static, computed on build. This means that if you change this data on runtime, you won’t get the updated values.

// Counter.swift import Foundation @objc(Counter)

class Counter: NSObject { @objc

func constantsToExport() -> [AnyHashable : Any]! {

return [

"number": 123.9,

"string": "foo",

"boolean": true,

"array": [1, 22.2, "33"],

"object": ["a": 1, "b": 2]

]

} }

Main queue setup warning

You might get a warning telling you that you didn’t implement the requiresMainQueueSetup method. This happens when you use constantsToExport or have implemented an init() method for UIKit components in React Native v0.49+.

To suppress this warning, you simply have to implement the specified method, and return a Boolean:

true if you need this class initialized on the main thread

if you need this class initialized on the main thread false if the class can be initialized on a background thread

// Counter.swift @objc(Counter)

class Counter: NSObject { ... @objc

static func requiresMainQueueSetup() -> Bool {

return true

} }

4. How to expose a Swift method

Let’s see how you can call Swift code from JS by adding a private variable and a method that increments it:

// Counter.swift import Foundation @objc(Counter)

class Counter: NSObject { private var count = 0 @objc

func increment() {

count += 1

print("count is \(count)")

} ... }

Important: we need to use the @objc directive on each method / property that needs to be called or accessed from Obj-C.

Next you have to expose the method to React Native’s bridge, from your Obj-C file, using the RCT_EXTERN_METHOD Macro and passing the method name:

// Counter.m ... @interface RCT_EXTERN_MODULE(Counter, NSObject) RCT_EXTERN_METHOD(increment) @end

Important: There is another very similar Macro, called RCT_EXPORT_METHOD , but that one is used with Objective-C code. We will use the one with EXTERN .

Now, if you call your Swift method from JS, it should print “ count is 1 ” in your Xcode output panel:

// App.js ... NativeModules.Counter.increment()

Rename your exposed method (optional)

You can also rename your method name if you want to expose it under a different identifier, using the _RCT_EXTERN_REMAP_METHOD Macro. Yes, it has a leading underscore.

// Counter.m ...

@interface ...

_RCT_EXTERN_REMAP_METHOD(inc, increment, false)

@end

Then, you should be able to access the method from JS as:

// App.js ... NativeModules.Counter.inc()

Note: the 3rd Bool argument of _RCT_EXTERN_REMAP_METHOD specifies if the method is synchronous or not. If you make it sync, by setting the flag to true, it will throw an error in DEBUG mode. Keep in mind that it’s NOT a good practice to write sync methods.

5. How to expose a Swift method with a callback

So far, you can call, from JS, some Swift methods that run native code. Next, let’s see how to get some data from Swift. Let’s create a new method that returns the count value:

// Counter.swift ... @objc

func getCount(_ callback: RCTResponseSenderBlock) {

callback([count])

} ...

Your getCount() method receives a callback argument that you can pass from your JS code. It will be executed with the arguments you will pass from Swift.

Important: you need to pass an Array to your callback.

Next, expose getCount to React Native’s bridge:

// Counter.m ... RCT_EXTERN_METHOD(increment)

RCT_EXTERN_METHOD(getCount: (RCTResponseSenderBlock)callback) ...

And, finally, call it in your JS file, by passing a callback function:

// App.js ... NativeModules.Counter.getCount(value => {

console.log("count is " + value)

})

This should print “ count is 1 ”, but this time in your JS console:

Passing multiple arguments to a callback (optional)

Need to pass more than one argument to your callback? No problem. Your callback param is just a NSArray object, so you can put anything in it:

// Counter.swift ... @objc

func getCount(_ callback: RCTResponseSenderBlock) {

callback([

count, // variable

123.9, // int or float

"third argument", // string

[1, 2.2, "3"], // array

["a": 1, "b": 2] // object

])

} ...

And this is how you can retrieve these arguments from JS:

// App.js ... NativeModules.Counter.getCount((first, ...others) => {

console.log("count is ", first)

console.log("other arguments ", others)

})

6. How to expose a Swift promise

Well, there are no built-in Promises in Swift, but you can expose a method that you can call from JS just like a Promise.

Let’s implement a decrement method that:

resolves by decrementing the count if it is greater than 0;

by decrementing the if it is greater than 0; rejects when the count is 0;

You have to use React Native’s types RCTPromiseResolveBlock and RCTPromiseRejectBlock and call the internal methods resolve() & reject() similarly to a JS Promise.

The resolve method has only 1 argument: the data that you want to resolve the Promise with.

The reject method has 3 arguments:

an error code : something specific to your domain logic;

: something specific to your domain logic; an error message : the rejection reason;

: the rejection reason; a NSError object: this is usually the error object you get when a network request fails;

// Counter.swift ... @objc

func decrement(

_ resolve: RCTPromiseResolveBlock,

rejecter reject: RCTPromiseRejectBlock

) -> Void { if (count == 0) {

let error = NSError(domain: "", code: 200, userInfo: nil)

reject("E_COUNT", "count cannot be negative", error)

} else {

count -= 1

resolve("count was decremented")

} } ...

Next, you need to expose this method to React Native’s bridge:

// Counter.m ... RCT_EXTERN_METHOD(

decrement: (RCTPromiseResolveBlock)resolve

rejecter: (RCTPromiseRejectBlock)reject

) ...

Note: if the above syntax looks weird, you might want to read some details on how RCT_EXTERN_METHOD works.

Finally, you can use it in your JS file by creating a function that wraps the Promise call and error handling:

// App.js ... NativeModules.Counter.increment() // create a function that wraps the Promise

function decrement() {

NativeModules.Counter.decrement()

.then(res => console.log(res))

.catch(e => console.log(e.message, e.code))

} decrement()

decrement()

Considering that you’ve previously called increment() once, your counter should be at 1. So, if you call decrement() twice:

the first time it should resolve and decrement the counter;

and decrement the counter; but the second time it should reject it, because the counter has reached 0;

Using async/await with Promises

Since you’re dealing with Promises, you can also use async/await. You could re-write the previous decrement function as:

// App.js ... // create an async function that wraps the Promise

async function decrement() {

try {

const res = await NativeModules.Counter.decrement()

console.log(res)

} catch(e) {

console.log(e.message, e.code)

}

} decrement()

decrement()

7. How to expose a Swift Event Emitter

You’ll often find it useful to be able to subscribe from JS to certain events that occur on the native side. For this, you’ll need an Event Emitter.

But first, you need some adjustments to your current bridge. You should extend your class from RCTEventEmitter instead of NSObject , which is an abstract class from React Native:

// Counter.m #import "React/RCTBridgeModule.h"

#import "React/RCTEventEmitter.h" @interface RCT_EXTERN_MODULE(Counter, RCTEventEmitter) ...

Then, you should also import the RCTEventEmitter header in your Bridging-Header file, so you can use it in your Swift class:

// CounterApp-Bridging-Header.h #import "React/RCTBridgeModule.h"

#import "React/RCTEventEmitter.h"

Now, you can implement your Swift Event Emitter as follows:

subclass RCTEventEmitter ;

; implement supportedEvents and return a list of event names;

and return a list of event names; call sendEvent to emit a specific event;

to emit a specific event; override constantsToExport and requiresMainQueueSetup in case you have implemented them;

In this app, let’s emit an onIncrement event every time the counter is incremented:

// Counter.swift @objc(Counter)

class Counter: RCTEventEmitter { ... @objc

func increment() {

count += 1

print("count is \(count)") sendEvent(withName: "onIncrement", body: ["count": count])

}

// we need to override this method and

// return an array of event names that we can listen to

override func supportedEvents() -> [String]! {

return ["onIncrement"]

}

// you also need to add the override attribute

// on these methods

override func constantsToExport() { ... }

override static func requiresMainQueueSetup() { ... } ... }

Note: since use override , we don't need to specify the @objc directive.

The final step is to subscribe to this event from JS. Add this code before you call the increment method, so that you subscribe to the event before it is emitted:

// App.js import {

NativeModules,

NativeEventEmitter

} from 'react-native' // instantiate the event emitter

const CounterEvents = new NativeEventEmitter(NativeModules.Counter) // subscribe to event

CounterEvents.addListener(

"onIncrement",

res => console.log("onIncrement event", res)

) NativeModules.Counter.increment() ...

8. How to extract your React Native module

It’s desirable that your application code consists only of React code, so that you don’t have to deal with React Native implementation details. That’s why it’s a good practice to extract your custom module into a separate file:

// Counter.js import { NativeModules, NativeEventEmitter } from 'react-native' class Counter extends NativeEventEmitter {

constructor(nativeModule) {

super(nativeModule); // explicitly set our custom methods and properties

this.initialCount = nativeModule.initialCount

this.getCount = nativeModule.getCount

this.increment = nativeModule.increment

this.decrement = async function() {

try {

const res = await nativeModule.decrement()

console.log(res)

} catch(e) {

console.log(e.message, e.code)

}

}

}

} export default new Counter(NativeModules.Counter)

And use it in your application as:

// App.js import Counter from "./Counter" Counter.addListener(

"onIncrement",

res => console.log("event onIncrement", res)

) Counter.increment() Counter.decrement()

Counter.decrement()

This way, you are hiding from your consumers the React Native integration, so that they can deal only with pure React code.

There are multiple ways to implement these modules. Check out the Counter.js file from the Github repo for more details.

Resources

The official documentation on Swift is almost non-existent and resources are scarce or outdated, due to frequent updates of React Native. However, there are some key resources, without which this article would not exist: