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.