Home Writing Reading

til / returning values from native modules in swift

While creating modules I found that the RCT_EXTERN_METHOD macro doesn’t handle returning values from the Swift function it binds to (it’s the same with @ReactMethod in Kotlin.)

They only support functions returning void. However, there are no errors in the IDE or when building. The returned value just becomes undefined in TypeScript when you call the function. I found it hard to debug why it didn’t work.

Here’s what we can do:

  • Export values using constantsToExport
  • Return values with a callback or promise

constantsToExport #

// DeviceInfoModule.m

#import <React/RCTBridgeModule.h>

@interface RCT_EXTERN_MODULE(DeviceInfoModule, NSObject)

// No exports here as we use constants

// I tried with RCT_EXTERN_METHOD(getBrand) which
// compiled, but it only returned undefined

+ (BOOL)requiresMainQueueSetup
{
   return NO;
}
@end

When overriding constantsToExport the documentation states that we should implement requiresMainQueueSetup. If we don’t require access to UIKit, then we should respond with NO.

// DeviceInfoModule.swift

@objc(DeviceInfoModule)
class DeviceInfoModule: NSObject {
  // Even though this returns a string, it's not
  // passed through the macro.
  func getBrand() -> String {
    return "Apple"
  }

  @objc
  func constantsToExport() -> [String: Any]! {
	// This will become an object in JavaScript/TypeScript
    return ["brand": getBrand()]
  }
}
// deviceInfo.ts

import { NativeModules } from 'react-native'
const { DeviceInfoModule } = NativeModules

interface DeviceInfoInterface {
  getBrand(): string
}

const deviceInfo = DeviceInfoModule.getConstants()

// Convenience methods, we could easily just export
// the constants directly if we wanted to.
export const DeviceInfo: DeviceInfoInterface = {
  getBrand: () => deviceInfo.brand,
}

Promise #

To handle promises we add some arguments to our method which makes it a tiny bit more complex, but the interface becomes simpler in TypeScript. Function coloring might screw that up though.

// DeviceInfoModule.m

#import <React/RCTBridgeModule.h>

@interface RCT_EXTERN_MODULE(DeviceInfoModule, NSObject)

// Define the method and set two arguments, resolve and reject
// If we want to send arguments, the resolve and reject
// need to be the last two parameters to work.
RCT_EXTERN_METHOD(getBrand:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)

@end
// DeviceInfoModule.swift

@objc(DeviceInfoModule)
class DeviceInfoModule: NSObject {
  func getBrand(_ resolve: RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) {
	resolve("Apple")
  }
}

Note the leading _ because the resolve parameter is not named in the RCT_EXTERN_METHOD above. This is how the macro works for all first arguments. I think that a named first parameter, for example, getBrand:resolver:(RCTPromiseResolveBlock)resolve would break it.

// deviceInfo.ts

import { NativeModules } from 'react-native'
const { DeviceInfoModule } = NativeModules

interface DeviceInfoInterface {
  getBrand(): Promise<string>
}

export default DeviceInfoModule as DeviceInfoInterface

Callback #

// DeviceInfoModule.m

#import <React/RCTBridgeModule.h>

@interface RCT_EXTERN_MODULE(DeviceInfoModule, NSObject)

// Define the method and set the callback
// Like with a promise, the callback needs to be the last argument
RCT_EXTERN_METHOD(getBrand:(RCTResponseSenderBlock)callback)

@end
// DeviceInfoModule.swift

@objc(DeviceInfoModule)
class DeviceInfoModule: NSObject {
  func getBrand(_ callback: RCTResponseSenderBlock) {
	// To return errors, we can follow Node's standard
	// of error first, data second. Or, we can split it into
	// two callbacks, one for success and one for error.
	callback([NSNull(), "Apple"])
  }
}

Note: We’re not allowed to use nil instead of NSNull() here.

// deviceInfo.ts

import { NativeModules } from 'react-native'
const { DeviceInfoModule } = NativeModules

interface DeviceInfoInterface {
  getBrand(callback: (err: Error, value: string) => void): void
}

export default DeviceInfoModule as DeviceInfoInterface

  • Loading next post...
  • Loading previous post...