Writing a React Native iOS module in Swift

The odds were against me: The documentation is a little slim. I haven't been paying attention to the javascript universe for 6 months. But I have emerged victorious. I can bridge with simple native code.

It didn't start well - I was copying and pasting code from tutorials all willy nilly. As a result, when I imported RCTBridgeModule, the module wasn’t found by XCode. However, I went back to the React docs and used that code, and that ended up working fine. I could swear I did that before...? Oh well, lesson learned. All part of the process.

React Native was really made to talk with Objective-C, so you'll need to create a bridging header. Those make your Swift API callable from Objective-C. The docs say to make this bridging header one way, and a couple of tutorials gave me this other way (probably older). I went with the other way because that was what I had in place when the thing started working.

Here is my bridging header:

//  Use this file to import your target's public headers that you would like to expose to Swift.

#import <React/RCTBridgeModule.h>
#import <React/RCTBridge.h>
#import <React/RCTEventDispatcher.h>
#import <React/RCTRootView.h>
#import <React/RCTUtils.h>
#import <React/RCTConvert.h>
#import <React/RCTBundleURLProvider.h>

I don't think you'll have to touch that too much once it's there.

This is the bridge that you make for each of the classes that you want to expose to React.

//  FooBridge.m

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


@interface RCT_EXTERN_MODULE(Foo, NSObject)

@end

That's for a Swift class that is called Foo. It has to subclass NSObject, and should begin like this. Note the @objc above the class declaration.

@objc(Foo)
class Foo: NSObject {

}

That’s all great. My objective-c knowledge is really terrible. The whole language seems really ugly, so I (self-destructively) avoid it as much as possible.

Adding methods with no arguments and no callbacks is pretty straightforward. Just add the method to the interface and call it from your javascript.

# Foo.swift

@objc(Foo)
class Foo: NSObject {  
  @objc func doThis() -> Void {
    // w00t
  }
}

# FooBridge.m

@interface RCT_EXTERN_MODULE(Foo, NSObject)
RCT_EXTERN_METHOD(doThis)  
@end

On the JS side, you basically just import your bridging module and call your method.

import { NativeEventEmitter, NativeModules } from 'react-native';  
const { Foo } = NativeModules;

export default class HomeView extends React.Component {  
  onDoThis() {
    Foo.doThis();
  }
}

Easy!

Passing information back to Javascript

Now, Swift can't return values directly to React, so we have to use callbacks. That makes more sense anyways, since a lot of the actions will probably be asynchronous.

I tried to add a method without looking much at the documentation, and ended up with an error like:

Exception ‘methodName’ is not a recognized Objective-C method

At this point, my bridging module looks like

@interface RCT_EXTERN_MODULE(Foo, NSObject)
RCT_EXTERN_METHOD(doThis)  
RCT_EXTERN_METHOD(download:(NSString*)fileUrl (RCTResponseSenderBlock)callback)  
@end

and the Swift method in question has a signature of

@objc func download(fileUrl: String, callback: RCTResponseSenderBlock) -> Void {}

The solution is to add an underscore to first param in the Swift method signature, as explained in this SO answer. I also had to change the placement of the callback param in the bridging module. So the new and almost correct signature and bridging bits:

# Swift signature
@objc func download(_ fileUrl: String, callback: RCTResponseSenderBlock) -> Void

# Bridging module
RCT_EXTERN_METHOD(download:(NSString*)fileUrl callback:(RCTResponseSenderBlock))  

That executes and gets me a new error (I really hope I’m hitting them all. That way, my SEO will skyrocket $$).

The error message is Argument 1 (RCTResponseSenderBlock) of Foo.download must not be null

I’m calling that method in my JS like so:

Foo.download("stringConstant", this.downloadCallback);  

Turns out that I need to add another something to my Swift method signature. What ended up working was this:

Foo {  
  ...
  @objc func download(_ fileUrl: String, callback: @escaping RCTResponseSenderBlock) -> Void {
    Downloader().download(fileURL: URL(string: fileUrl)!, callback: { error, args in
      let destinationPath = args["destinationPath"]
      print("Destination URL: \(destinationPath)")
      callback([NSNull(), [
        "destinationPath": destinationPath
      ]])
    })

  }
}

I needed to add the @escaping to let Swift know that the callback might be executed after the function returns.

Now I can pass those values back to my JS, which looks like this:

export default class HomeView extends React.Component {  
    onDownload() {
        let fileUrl = "myFile.jpg";
        Foo.download(fileUrl, function(error, response) {
            let downloadedPath = response.destinationPath;
            console.log("Success!", downloadedPath);
        });
    }
}

There! Hope that gets you either started or past any ruts you might be in.