A Step-By-Step Guide to Swift Native Modules

If you don't have any prior experience with native development, getting started with native modules can seem intimidating. The React Native docs do a good job introducing native iOS modules, and this guide is not intended to replace what they've provided. Instead, I hope it supplements the docs as an additional resource.

My aim here is to show you how to write native iOS modules in Swift by starting with the simplest (albeit useless) example possible, and build upon our knowledge gradually until we've created something interesting and useful. The end result will be a module that exports functionality from the iOS Vision APIs to perform computer-vision tasks like text and face recognition.

Step One — the simplest module ever

Here’s the simplest module I can imagine. It exports a method that simply prints a message to the iOS console…

1//
2//  RCTVisionModule.swift
3//  VisionNativeModulePOC
4//
5//  Created by Tanner West on 6/2/23.
6//
7import Foundation
8
9@objc(RCTVisionModule)
10class RCTVisionModule: NSObject {
11  @objc(sayHello)
12  func sayHello() {
13    print("TANNER SAYS HELLO")
14  }
15}
1//
2//  RCTVisionModule.m
3//  VisionNativeModulePOC
4//
5//  Created by Tanner West on 6/2/23.
6//
7
8#import <Foundation/Foundation.h>
9#import <React/RCTBridgeModule.h>
10
11@interface RCT_EXTERN_MODULE(RCTVisionModule, NSObject)
12RCT_EXTERN_METHOD(sayHello);
13@end

To get started on a strong foundation, let's make sure we understand what each of these two files are doing.

RCTVisionModule.swift is where the heart of the code lies. It declares a class of the type NSObject with a single method, sayHello that simply prints a message to the Xcode console. Most of the code should look pretty straightforward, but you may be wondering about the lines beginning with @objc. What's going on there? Those are Swift attributes that make our Swift code available in Objective-C code. But why do we need to do that? That brings us to our other file.

RCTVisionModule.m is where we tell React Native about our native code. RCT_EXTERN_MODULE and RCT_EXTERN_METHOD are both Objective-C macros that register our native code with React Native.

To use our exported method in our React Native code, we simply import our native module.

1import React, {FC} from 'react';
2import {Button, NativeModules, SafeAreaView} from 'react-native';
3const {VisionModule} = NativeModules;
4
5const App: FC = () => {
6  return (
7    <SafeAreaView>
8      <Button title="Say Hello" onPress={() => VisionModule?.sayHello()} />
9    </SafeAreaView>
10  );
11};
12
13export default App;

We should then see our message logged to the console when we run our app from Xcode

Step Two — passing arguments into our native module

The method we implemented in Step One helped us understand the fundamentals of native Swift modules. Let's take it a step further and pass some data into our native module.

You can pass several data types between JavaScript and your native module. See the full list here. Let's add support for a string argument and a number argument in our native module.

1
2//
3//  RCTVisionModule.m
4//  VisionNativeModulePOC
5//
6//  Created by Tanner West on 6/2/23.
7//
8
9#import <Foundation/Foundation.h>
10#import <React/RCTBridgeModule.h>
11
12@interface RCT_EXTERN_MODULE(RCTVisionModule, NSObject)
13RCT_EXTERN_METHOD(sayHello: (NSString *)specialMessage specialNumber: (nonnull NSNumber *)specialNumber);
14@end
1
2//
3//  RCTVisionModule.swift
4//  VisionNativeModulePOC
5//
6//  Created by Tanner West on 6/2/23.
7//
8import Foundation
9
10@objc(RCTVisionModule)
11class RCTVisionModule: NSObject {
12  @objc(sayHello:specialNumber:)
13  func sayHello(_ specialMessage: String, specialNumber: NSNumber) {
14    print("read this special message: ", specialMessage)
15    print("look at this special number: ", specialNumber)  }
16}

Now, in our React Native code, we can pass two arguments to our sayHello() method:

1import React, {FC} from 'react';
2import {Button, NativeModules, SafeAreaView} from 'react-native';
3const {VisionModule} = NativeModules;
4
5const App: FC = () => {
6  return (
7    <SafeAreaView>
8      <Button
9        title="Say Hello"
10        onPress={() => VisionModule?.sayHello('this message is special', 1764)}
11      />
12    </SafeAreaView>
13  );
14};
15
16export default App;

And now we'll see the arguments logged to the Xcode console.

Step Three — passing data back to React Native via callbacks

In Step Two, we made our module a little more interesting by passing arguments from our React Native code into our native code. Now let's learn how to pass data back to React Native. The simplest way to achieve this is via callbacks.

1//
2//  RCTVisionModule.m
3//  VisionNativeModulePOC
4//
5//  Created by Tanner West on 6/2/23.
6//
7
8#import <Foundation/Foundation.h>
9#import <React/RCTBridgeModule.h>
10
11@interface RCT_EXTERN_MODULE(RCTVisionModule, NSObject)
12RCT_EXTERN_METHOD(sayHello: (NSString *)specialMessage specialNumber: (nonnull NSNumber *)specialNumber callback: (RCTResponseSenderBlock)callback);
13@end
1//
2//  RCTVisionModule.swift
3//  VisionNativeModulePOC
4//
5//  Created by Tanner West on 6/2/23.
6//
7
8import Foundation
9
10@objc(RCTVisionModule)
11class RCTVisionModule: NSObject {
12  @objc(sayHello:specialNumber:callback:)
13  func sayHello(_ specialMessage: String, specialNumber: NSNumber, callback: RCTResponseSenderBlock) {
14    print("read this special message: ", specialMessage)
15    print("look at this special number: ", specialNumber)
16    let sum = specialNumber.decimalValue + 5
17    callback([sum, "the first arg is the special sum; this is just a string"])
18  }
19}

Here, we add a parameter named callback to our module interface, which is is of the type RCTResponseSenderBlock. In our Swift code, we can call the function with an array of data. On the React Native side, our callback will be invoked with each element array as a separate argument:

1import React, {FC} from 'react';
2import {Button, NativeModules, SafeAreaView} from 'react-native';
3const {VisionModule} = NativeModules;
4
5const App: FC = () => {
6  const callback = (firstArg: any, secondArg: any) => {
7    console.log('callback args: ', firstArg, secondArg);
8  };
9  return (
10    <SafeAreaView>
11      <Button
12        title="Say Hello"
13        onPress={() =>
14          VisionModule?.sayHello('this message is special', 1764, callback)
15        }
16      />
17    </SafeAreaView>
18  );
19};
20
21export default App;

The result in our JavaScript console:

Step Four - Calling an iOS Vision method

Now that we understand the basics of passing data back and forth with our Native Modules, we're going to take a big leap and build something that might actually be useful by implementing a couple of methods that execute computer vision requests with the iOS Vision library and returns the result.

1//
2//  RCTVisionModule.m
3//  VisionNativeModulePOC
4//
5//  Created by Tanner West on 6/2/23.
6//
7
8#import <Foundation/Foundation.h>
9#import <React/RCTBridgeModule.h>
10
11@interface RCT_EXTERN_MODULE(RCTVisionModule, NSObject)
12RCT_EXTERN_METHOD(detectText: (NSString *)imageUrl callback: (RCTResponseSenderBlock)callback);
13RCT_EXTERN_METHOD(detectFaces: (NSString *)imageUrl callback: (RCTResponseSenderBlock)callback);
14@end
1//
2//  RCTVisionModule.swift
3//  VisionNativeModulePOC
4//
5//  Created by Tanner West on 6/2/23.
6//
7
8import Foundation
9import CoreImage
10import Vision
11
12@objc(RCTVisionModule)
13class RCTVisionModule: NSObject {
14  
15  func createCIImage(from imageURL: URL) -> CIImage? {
16    guard let ciImage = CIImage(contentsOf: imageURL) else {
17      // TODO handle error
18      return nil
19    }
20    return ciImage
21  }
22  
23  @objc(detectText:callback:)
24  func detectText(_ imgUrl: String, callback: @escaping RCTResponseSenderBlock) {
25    
26    if let imageURL = URL(string: imgUrl) {
27      if let ciImage = createCIImage(from: imageURL) {
28        let requestHandler = VNImageRequestHandler(ciImage: ciImage)
29        let request = VNRecognizeTextRequest(completionHandler: { request, error in
30          if let error = error {
31            // TODO handle error
32          } else if let results = request.results as? [VNRecognizedTextObservation] {
33            var result: [String] = []
34            for observation in results {
35              if let recognizedText = observation.topCandidates(1).first {
36                result.append(recognizedText.string)
37              }
38            }
39            callback([result])
40          }
41        })
42        
43        do {
44          try requestHandler.perform([request])
45        } catch {
46          // TODO handle error
47        }
48      }
49    }
50  }
51  
52  @objc(detectFaces:callback:)
53  func detectFaces(_ imgUrl: String, callback: @escaping RCTResponseSenderBlock) {
54    
55    if let imageURL = URL(string: imgUrl) {
56      if let ciImage = createCIImage(from: imageURL) {
57        
58        let requestHandler = VNImageRequestHandler(ciImage: ciImage)
59        let request = VNDetectFaceRectanglesRequest(completionHandler: { request, error in
60          if let error = error {
61            // TODO handle error
62            print(error)
63          } else if let results = request.results as? [VNFaceObservation] {
64            var result: [Dictionary<String, CGFloat>] = []
65            var resultsLengthString = String(results.count)
66            for observation in results {
67              let box = observation.boundingBox
68              let boxDict = ["width": box.size.width,
69                            "height": box.size.height,
70                                 "x": box.origin.x,
71                                 "y": box.origin.y]
72              result.append(boxDict)
73            }
74            callback([result])
75          }
76        })
77        
78        do {
79          try requestHandler.perform([request])
80        } catch {
81          // TODO handle error
82          print(error)
83        }
84      }
85    }
86  }
87}

Obviously, we've added a ton of native Swift code in this most recent revision. I won't explain that code in depth here, but you can check out the iOS Vision documentation to learn more. The important thing is that by now, you should see how we get the results of our Vision API requests back to our React Native app via callbacks.